Commit 7bfbd890 authored by Phil Hughes's avatar Phil Hughes

Merge branch '30355-use-hours-only-for-time-tracking-ee' into 'master'

[EE] Limit time tracking units to hours

See merge request gitlab-org/gitlab-ee!14228
parents 72bf00c7 b36db157
...@@ -38,6 +38,7 @@ export default Vue.extend({ ...@@ -38,6 +38,7 @@ export default Vue.extend({
issue: {}, issue: {},
list: {}, list: {},
loadingAssignees: false, loadingAssignees: false,
timeTrackingLimitToHours: boardsStore.timeTracking.limitToHours,
}; };
}, },
computed: { computed: {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlTooltip } from '@gitlab/ui'; import { GlTooltip } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility'; import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import boardsStore from '../stores/boards_store';
export default { export default {
components: { components: {
...@@ -14,12 +15,17 @@ export default { ...@@ -14,12 +15,17 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
limitToHours: boardsStore.timeTracking.limitToHours,
};
},
computed: { computed: {
title() { title() {
return stringifyTime(parseSeconds(this.estimate), true); return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true);
}, },
timeEstimate() { timeEstimate() {
return stringifyTime(parseSeconds(this.estimate)); return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }));
}, },
}, },
}; };
......
...@@ -56,6 +56,7 @@ export default () => { ...@@ -56,6 +56,7 @@ export default () => {
} }
boardsStore.create(); boardsStore.create();
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
issueBoardsApp = new Vue({ issueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
......
...@@ -12,6 +12,9 @@ import eventHub from '../eventhub'; ...@@ -12,6 +12,9 @@ import eventHub from '../eventhub';
const boardsStore = { const boardsStore = {
disabled: false, disabled: false,
timeTracking: {
limitToHours: false,
},
scopedLabels: { scopedLabels: {
helpLink: '', helpLink: '',
enabled: false, enabled: false,
...@@ -222,6 +225,10 @@ const boardsStore = { ...@@ -222,6 +225,10 @@ const boardsStore = {
setIssueDetail(issueDetail) { setIssueDetail(issueDetail) {
this.detail.issue = issueDetail; this.detail.issue = issueDetail;
}, },
setTimeTrackingLimitToHours(limitToHours) {
this.timeTracking.limitToHours = parseBoolean(limitToHours);
},
}; };
BoardsStoreEE.initEESpecific(boardsStore); BoardsStoreEE.initEESpecific(boardsStore);
......
...@@ -479,9 +479,13 @@ export const pikadayToString = date => { ...@@ -479,9 +479,13 @@ export const pikadayToString = date => {
* Seconds can be negative or positive, zero or non-zero. Can be configured for any day * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
* or week length. * or week length.
*/ */
export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => { export const parseSeconds = (
seconds,
{ daysPerWeek = 5, hoursPerDay = 8, limitToHours = false } = {},
) => {
const DAYS_PER_WEEK = daysPerWeek; const DAYS_PER_WEEK = daysPerWeek;
const HOURS_PER_DAY = hoursPerDay; const HOURS_PER_DAY = hoursPerDay;
const SECONDS_PER_MINUTE = 60;
const MINUTES_PER_HOUR = 60; const MINUTES_PER_HOUR = 60;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
...@@ -493,9 +497,18 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) ...@@ -493,9 +497,18 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
minutes: 1, minutes: 1,
}; };
let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR); if (limitToHours) {
timePeriodConstraints.weeks = 0;
timePeriodConstraints.days = 0;
}
let unorderedMinutes = Math.abs(seconds / SECONDS_PER_MINUTE);
return _.mapObject(timePeriodConstraints, minutesPerPeriod => { return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
if (minutesPerPeriod === 0) {
return 0;
}
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= periodCount * minutesPerPeriod; unorderedMinutes -= periodCount * minutesPerPeriod;
......
...@@ -28,11 +28,16 @@ export default { ...@@ -28,11 +28,16 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
limitToHours: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
parsedTimeRemaining() { parsedTimeRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent; const diffSeconds = this.timeEstimate - this.timeSpent;
return parseSeconds(diffSeconds); return parseSeconds(diffSeconds, { limitToHours: this.limitToHours });
}, },
timeRemainingHumanReadable() { timeRemainingHumanReadable() {
return stringifyTime(this.parsedTimeRemaining); return stringifyTime(this.parsedTimeRemaining);
......
...@@ -53,6 +53,7 @@ export default { ...@@ -53,6 +53,7 @@ export default {
:time-spent="store.totalTimeSpent" :time-spent="store.totalTimeSpent"
:human-time-estimate="store.humanTimeEstimate" :human-time-estimate="store.humanTimeEstimate"
:human-time-spent="store.humanTotalTimeSpent" :human-time-spent="store.humanTotalTimeSpent"
:limit-to-hours="store.timeTrackingLimitToHours"
:root-path="store.rootPath" :root-path="store.rootPath"
/> />
</div> </div>
......
...@@ -37,6 +37,10 @@ export default { ...@@ -37,6 +37,10 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
limitToHours: {
type: Boolean,
default: false,
},
rootPath: { rootPath: {
type: String, type: String,
required: true, required: true,
...@@ -129,6 +133,7 @@ export default { ...@@ -129,6 +133,7 @@ export default {
:time-spent="timeSpent" :time-spent="timeSpent"
:time-spent-human-readable="humanTimeSpent" :time-spent-human-readable="humanTimeSpent"
:time-estimate-human-readable="humanTimeEstimate" :time-estimate-human-readable="humanTimeEstimate"
:limit-to-hours="limitToHours"
/> />
<transition name="help-state-toggle"> <transition name="help-state-toggle">
<time-tracking-help-state v-if="showHelpState" :root-path="rootPath" /> <time-tracking-help-state v-if="showHelpState" :root-path="rootPath" />
......
import Vue from 'vue'; import Vue from 'vue';
import timeTracker from './components/time_tracking/time_tracker.vue'; import timeTracker from './components/time_tracking/time_tracker.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
export default class SidebarMilestone { export default class SidebarMilestone {
constructor() { constructor() {
...@@ -7,7 +8,7 @@ export default class SidebarMilestone { ...@@ -7,7 +8,7 @@ export default class SidebarMilestone {
if (!el) return; if (!el) return;
const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = el.dataset; const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent, limitToHours } = el.dataset;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
...@@ -22,6 +23,7 @@ export default class SidebarMilestone { ...@@ -22,6 +23,7 @@ export default class SidebarMilestone {
timeSpent: parseInt(timeSpent, 10), timeSpent: parseInt(timeSpent, 10),
humanTimeEstimate, humanTimeEstimate,
humanTimeSpent, humanTimeSpent,
limitToHours: parseBoolean(limitToHours),
rootPath: '/', rootPath: '/',
}, },
}), }),
......
...@@ -8,7 +8,7 @@ export default class SidebarStore { ...@@ -8,7 +8,7 @@ export default class SidebarStore {
} }
initSingleton(options) { initSingleton(options) {
const { currentUser, rootPath, editable } = options; const { currentUser, rootPath, editable, timeTrackingLimitToHours } = options;
this.currentUser = currentUser; this.currentUser = currentUser;
this.rootPath = rootPath; this.rootPath = rootPath;
this.editable = editable; this.editable = editable;
...@@ -16,6 +16,7 @@ export default class SidebarStore { ...@@ -16,6 +16,7 @@ export default class SidebarStore {
this.totalTimeSpent = 0; this.totalTimeSpent = 0;
this.humanTimeEstimate = ''; this.humanTimeEstimate = '';
this.humanTimeSpent = ''; this.humanTimeSpent = '';
this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = []; this.assignees = [];
this.isFetching = { this.isFetching = {
assignees: true, assignees: true,
......
...@@ -253,6 +253,7 @@ module ApplicationSettingsHelper ...@@ -253,6 +253,7 @@ module ApplicationSettingsHelper
:throttle_unauthenticated_enabled, :throttle_unauthenticated_enabled,
:throttle_unauthenticated_period_in_seconds, :throttle_unauthenticated_period_in_seconds,
:throttle_unauthenticated_requests_per_period, :throttle_unauthenticated_requests_per_period,
:time_tracking_limit_to_hours,
:two_factor_grace_period, :two_factor_grace_period,
:unique_ips_limit_enabled, :unique_ips_limit_enabled,
:unique_ips_limit_per_user, :unique_ips_limit_per_user,
......
...@@ -14,7 +14,8 @@ module BoardsHelper ...@@ -14,7 +14,8 @@ module BoardsHelper
issue_link_base: build_issue_link_base, issue_link_base: build_issue_link_base,
root_path: root_path, root_path: root_path,
bulk_update_path: @bulk_issues_path, bulk_update_path: @bulk_issues_path,
default_avatar: image_path(default_avatar) default_avatar: image_path(default_avatar),
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s
} }
end end
......
...@@ -430,7 +430,8 @@ module IssuablesHelper ...@@ -430,7 +430,8 @@ module IssuablesHelper
editable: issuable.dig(:current_user, :can_edit), editable: issuable.dig(:current_user, :can_edit),
currentUser: issuable[:current_user], currentUser: issuable[:current_user],
rootPath: root_path, rootPath: root_path,
fullPath: issuable[:project_full_path] fullPath: issuable[:project_full_path],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours
} }
end end
......
...@@ -82,6 +82,7 @@ module ApplicationSettingImplementation ...@@ -82,6 +82,7 @@ module ApplicationSettingImplementation
throttle_unauthenticated_enabled: false, throttle_unauthenticated_enabled: false,
throttle_unauthenticated_period_in_seconds: 3600, throttle_unauthenticated_period_in_seconds: 3600,
throttle_unauthenticated_requests_per_period: 3600, throttle_unauthenticated_requests_per_period: 3600,
time_tracking_limit_to_hours: false,
two_factor_grace_period: 48, two_factor_grace_period: 48,
unique_ips_limit_enabled: false, unique_ips_limit_enabled: false,
unique_ips_limit_per_user: 10, unique_ips_limit_per_user: 10,
......
...@@ -8,4 +8,11 @@ ...@@ -8,4 +8,11 @@
.form-text.text-muted .form-text.text-muted
= _('Default first day of the week in calendars and date pickers.') = _('Default first day of the week in calendars and date pickers.')
.form-group
= f.label :time_tracking, _('Time tracking'), class: 'label-bold'
.form-check
= f.check_box :time_tracking_limit_to_hours, class: 'form-check-input'
= f.label :time_tracking_limit_to_hours, class: 'form-check-label' do
= _('Limit display of time tracking units to hours.')
= f.submit _('Save changes'), class: "btn btn-success" = f.submit _('Save changes'), class: "btn btn-success"
...@@ -3,4 +3,5 @@ ...@@ -3,4 +3,5 @@
":time-spent" => "issue.timeSpent || 0", ":time-spent" => "issue.timeSpent || 0",
":human-time-estimate" => "issue.humanTimeEstimate", ":human-time-estimate" => "issue.humanTimeEstimate",
":human-time-spent" => "issue.humanTimeSpent", ":human-time-spent" => "issue.humanTimeSpent",
":limit-to-hours" => "timeTrackingLimitToHours",
"root-path" => "#{root_url}" } "root-path" => "#{root_url}" }
...@@ -93,7 +93,11 @@ ...@@ -93,7 +93,11 @@
= milestone.issues_visible_to_user(current_user).closed.count = milestone.issues_visible_to_user(current_user).closed.count
.block .block
#issuable-time-tracker{ data: { time_estimate: @milestone.total_issue_time_estimate, time_spent: @milestone.total_issue_time_spent, human_time_estimate: @milestone.human_total_issue_time_estimate, human_time_spent: @milestone.human_total_issue_time_spent } } #issuable-time-tracker{ data: { time_estimate: @milestone.total_issue_time_estimate,
time_spent: @milestone.total_issue_time_spent,
human_time_estimate: @milestone.human_total_issue_time_estimate,
human_time_spent: @milestone.human_total_issue_time_spent,
limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s } }
// Fallback while content is loading // Fallback while content is loading
.title.hide-collapsed .title.hide-collapsed
= _('Time tracking') = _('Time tracking')
......
---
title: Add option to limit time tracking units to hours
merge_request: 29469
author: Jon Kolb
type: added
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddTimeTrackingLimitToHoursToApplicationSettings < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default :application_settings, :time_tracking_limit_to_hours, :boolean, default: false, allow_null: false
end
def down
remove_column :application_settings, :time_tracking_limit_to_hours
end
end
...@@ -229,6 +229,7 @@ ActiveRecord::Schema.define(version: 20190620112608) do ...@@ -229,6 +229,7 @@ ActiveRecord::Schema.define(version: 20190620112608) do
t.boolean "dns_rebinding_protection_enabled", default: true, null: false t.boolean "dns_rebinding_protection_enabled", default: true, null: false
t.boolean "default_project_deletion_protection", default: false, null: false t.boolean "default_project_deletion_protection", default: false, null: false
t.boolean "lock_memberships_to_ldap", default: false, null: false t.boolean "lock_memberships_to_ldap", default: false, null: false
t.boolean "time_tracking_limit_to_hours", default: false, null: false
t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree t.index ["custom_project_templates_group_id"], name: "index_application_settings_on_custom_project_templates_group_id", using: :btree
t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree t.index ["file_template_project_id"], name: "index_application_settings_on_file_template_project_id", using: :btree
t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree t.index ["usage_stats_set_by_user_id"], name: "index_application_settings_on_usage_stats_set_by_user_id", using: :btree
......
...@@ -273,6 +273,7 @@ are listed in the descriptions of the relevant settings. ...@@ -273,6 +273,7 @@ are listed in the descriptions of the relevant settings.
| `throttle_unauthenticated_enabled` | boolean | no | (**If enabled, requires:** `throttle_unauthenticated_period_in_seconds` and `throttle_unauthenticated_requests_per_period`) Enable unauthenticated request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). | | `throttle_unauthenticated_enabled` | boolean | no | (**If enabled, requires:** `throttle_unauthenticated_period_in_seconds` and `throttle_unauthenticated_requests_per_period`) Enable unauthenticated request rate limit. Helps reduce request volume (e.g. from crawlers or abusive bots). |
| `throttle_unauthenticated_period_in_seconds` | integer | required by: `throttle_unauthenticated_enabled` | Rate limit period in seconds. | | `throttle_unauthenticated_period_in_seconds` | integer | required by: `throttle_unauthenticated_enabled` | Rate limit period in seconds. |
| `throttle_unauthenticated_requests_per_period` | integer | required by: `throttle_unauthenticated_enabled` | Max requests per period per IP. | | `throttle_unauthenticated_requests_per_period` | integer | required by: `throttle_unauthenticated_enabled` | Max requests per period per IP. |
| `time_tracking_limit_to_hours` | boolean | no | Limit display of time tracking units to hours. Default is `false`. |
| `two_factor_grace_period` | integer | required by: `require_two_factor_authentication` | Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication. | | `two_factor_grace_period` | integer | required by: `require_two_factor_authentication` | Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication. |
| `unique_ips_limit_enabled` | boolean | no | (**If enabled, requires:** `unique_ips_limit_per_user` and `unique_ips_limit_time_window`) Limit sign in from multiple ips. | | `unique_ips_limit_enabled` | boolean | no | (**If enabled, requires:** `unique_ips_limit_per_user` and `unique_ips_limit_time_window`) Limit sign in from multiple ips. |
| `unique_ips_limit_per_user` | integer | required by: `unique_ips_limit_enabled` | Maximum number of ips per user. | | `unique_ips_limit_per_user` | integer | required by: `unique_ips_limit_enabled` | Maximum number of ips per user. |
......
...@@ -73,7 +73,15 @@ The following time units are available: ...@@ -73,7 +73,15 @@ The following time units are available:
Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h. Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h.
Other interesting links: ### Limit displayed units to hours
> Introduced in GitLab 12.0.
The display of time units can be limited to hours through the option in **Admin Area > Settings > Preferences** under 'Localization'.
With this option enabled, `75h` is displayed instead of `1w 4d 3h`.
## Other interesting links
- [Time Tracking landing page on about.gitlab.com](https://about.gitlab.com/solutions/time-tracking/) - [Time Tracking landing page on about.gitlab.com](https://about.gitlab.com/solutions/time-tracking/)
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
def parse(string) def parse(string)
with_custom_config do with_custom_config do
string.sub!(/\A-/, '') string = string.sub(/\A-/, '')
seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil
seconds *= -1 if seconds && Regexp.last_match seconds *= -1 if seconds && Regexp.last_match
...@@ -16,10 +16,12 @@ module Gitlab ...@@ -16,10 +16,12 @@ module Gitlab
def output(seconds) def output(seconds)
with_custom_config do with_custom_config do
ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil ChronicDuration.output(seconds, format: :short, limit_to_hours: limit_to_hours_setting, weeks: true) rescue nil
end end
end end
private
def with_custom_config def with_custom_config
# We may want to configure it through project settings in a future version. # We may want to configure it through project settings in a future version.
ChronicDuration.hours_per_day = 8 ChronicDuration.hours_per_day = 8
...@@ -32,5 +34,9 @@ module Gitlab ...@@ -32,5 +34,9 @@ module Gitlab
result result
end end
def limit_to_hours_setting
Gitlab::CurrentSettings.time_tracking_limit_to_hours
end
end end
end end
...@@ -7892,6 +7892,9 @@ msgstr "" ...@@ -7892,6 +7892,9 @@ msgstr ""
msgid "Licenses" msgid "Licenses"
msgstr "" msgstr ""
msgid "Limit display of time tracking units to hours."
msgstr ""
msgid "Limit namespaces and projects that can be indexed" msgid "Limit namespaces and projects that can be indexed"
msgstr "" msgstr ""
......
...@@ -16,7 +16,9 @@ describe 'Issue Boards', :js do ...@@ -16,7 +16,9 @@ describe 'Issue Boards', :js do
let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) } let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) } let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) } let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card) { find('.board:nth-child(2)').first('.board-card') } let(:card) { find('.board:nth-child(2)').first('.board-card') }
let(:application_settings) { {} }
around do |example| around do |example|
Timecop.freeze { example.run } Timecop.freeze { example.run }
...@@ -27,6 +29,8 @@ describe 'Issue Boards', :js do ...@@ -27,6 +29,8 @@ describe 'Issue Boards', :js do
sign_in(user) sign_in(user)
stub_application_setting(application_settings)
visit project_board_path(project, board) visit project_board_path(project, board)
wait_for_requests wait_for_requests
end end
...@@ -223,16 +227,24 @@ describe 'Issue Boards', :js do ...@@ -223,16 +227,24 @@ describe 'Issue Boards', :js do
end end
context 'time tracking' do context 'time tracking' do
let(:compare_meter_tooltip) { find('.time-tracking .time-tracking-content .compare-meter')['data-original-title'] }
before do before do
issue2.timelogs.create(time_spent: 14400, user: user) issue2.timelogs.create(time_spent: 14400, user: user)
issue2.update!(time_estimate: 28800) issue2.update!(time_estimate: 128800)
click_card(card)
end end
it 'shows time tracking progress bar' do it 'shows time tracking progress bar' do
click_card(card) expect(compare_meter_tooltip).to eq('Time remaining: 3d 7h 46m')
end
context 'when time_tracking_limit_to_hours is true' do
let(:application_settings) { { time_tracking_limit_to_hours: true } }
page.within('.time-tracking') do it 'shows time tracking progress bar' do
expect(find('.time-tracking-content .compare-meter')['data-original-title']).to eq('Time remaining: 4h') expect(compare_meter_tooltip).to eq('Time remaining: 31h 46m')
end end
end end
end end
......
...@@ -334,6 +334,12 @@ describe('prettyTime methods', () => { ...@@ -334,6 +334,12 @@ describe('prettyTime methods', () => {
assertTimeUnits(aboveOneDay, 33, 2, 2, 0); assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
assertTimeUnits(aboveOneWeek, 26, 0, 1, 9); assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
}); });
it('should correctly parse values when limitedToHours is true', () => {
const twoDays = datetimeUtility.parseSeconds(173000, { limitToHours: true });
assertTimeUnits(twoDays, 3, 48, 0, 0);
});
}); });
describe('stringifyTime', () => { describe('stringifyTime', () => {
......
...@@ -355,4 +355,14 @@ describe('Store', () => { ...@@ -355,4 +355,14 @@ describe('Store', () => {
expect(boardsStore.moving.list).toEqual(dummyList); expect(boardsStore.moving.list).toEqual(dummyList);
}); });
}); });
describe('setTimeTrackingLimitToHours', () => {
it('sets the timeTracking.LimitToHours option', () => {
boardsStore.timeTracking.limitToHours = false;
boardsStore.setTimeTrackingLimitToHours('true');
expect(boardsStore.timeTracking.limitToHours).toEqual(true);
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue'; import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
import boardsStore from '~/boards/stores/boards_store';
import mountComponent from '../../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Issue Tine Estimate component', () => { describe('Issue Time Estimate component', () => {
let vm; let vm;
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(IssueTimeEstimate); boardsStore.create();
vm = mountComponent(Component, {
estimate: 374460,
});
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
}); });
it('renders the correct time estimate', () => { describe('when limitToHours is false', () => {
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m'); beforeEach(() => {
}); boardsStore.timeTracking.limitToHours = false;
const Component = Vue.extend(IssueTimeEstimate);
vm = mountComponent(Component, {
estimate: 374460,
});
});
it('renders the correct time estimate', () => {
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
});
it('renders expanded time estimate in tooltip', () => {
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
'2 weeks 3 days 1 minute',
);
});
it('prevents tooltip xss', done => {
const alertSpy = spyOn(window, 'alert');
vm.estimate = 'Foo <script>alert("XSS")</script>';
it('renders expanded time estimate in tooltip', () => { vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain( expect(alertSpy).not.toHaveBeenCalled();
'2 weeks 3 days 1 minute', expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
); expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
done();
});
});
}); });
it('prevents tooltip xss', done => { describe('when limitToHours is true', () => {
const alertSpy = spyOn(window, 'alert'); beforeEach(() => {
vm.estimate = 'Foo <script>alert("XSS")</script>'; boardsStore.timeTracking.limitToHours = true;
const Component = Vue.extend(IssueTimeEstimate);
vm = mountComponent(Component, {
estimate: 374460,
});
});
it('renders the correct time estimate', () => {
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('104h 1m');
});
vm.$nextTick(() => { it('renders expanded time estimate in tooltip', () => {
expect(alertSpy).not.toHaveBeenCalled(); expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m'); '104 hours 1 minute',
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m'); );
done();
}); });
}); });
}); });
...@@ -13,6 +13,7 @@ describe('Issuable Time Tracker', () => { ...@@ -13,6 +13,7 @@ describe('Issuable Time Tracker', () => {
timeSpent, timeSpent,
timeEstimateHumanReadable, timeEstimateHumanReadable,
timeSpentHumanReadable, timeSpentHumanReadable,
limitToHours,
}) => { }) => {
setFixtures(` setFixtures(`
<div> <div>
...@@ -25,6 +26,7 @@ describe('Issuable Time Tracker', () => { ...@@ -25,6 +26,7 @@ describe('Issuable Time Tracker', () => {
timeSpent, timeSpent,
humanTimeEstimate: timeEstimateHumanReadable, humanTimeEstimate: timeEstimateHumanReadable,
humanTimeSpent: timeSpentHumanReadable, humanTimeSpent: timeSpentHumanReadable,
limitToHours: Boolean(limitToHours),
rootPath: '/', rootPath: '/',
}; };
...@@ -128,6 +130,29 @@ describe('Issuable Time Tracker', () => { ...@@ -128,6 +130,29 @@ describe('Issuable Time Tracker', () => {
}); });
}); });
describe('Comparison pane when limitToHours is true', () => {
beforeEach(() => {
initTimeTrackingComponent({
timeEstimate: 100000, // 1d 3h
timeSpent: 5000, // 1h 23m
timeEstimateHumanReadable: '',
timeSpentHumanReadable: '',
limitToHours: true,
});
});
it('should show the correct tooltip text', done => {
Vue.nextTick(() => {
expect(vm.showComparisonState).toBe(true);
const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset
.originalTitle;
expect($title).toBe('Time remaining: 26h 23m');
done();
});
});
});
describe('Estimate only pane', () => { describe('Estimate only pane', () => {
beforeEach(() => { beforeEach(() => {
initTimeTrackingComponent({ initTimeTrackingComponent({
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::TimeTrackingFormatter do
describe '#parse' do
subject { described_class.parse(duration_string) }
context 'positive durations' do
let(:duration_string) { '3h 20m' }
it { expect(subject).to eq(12_000) }
end
context 'negative durations' do
let(:duration_string) { '-3h 20m' }
it { expect(subject).to eq(-12_000) }
end
end
describe '#output' do
let(:num_seconds) { 178_800 }
subject { described_class.output(num_seconds) }
context 'time_tracking_limit_to_hours setting is true' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
end
it { expect(subject).to eq('49h 40m') }
end
context 'time_tracking_limit_to_hours setting is false' do
before do
stub_application_setting(time_tracking_limit_to_hours: false)
end
it { expect(subject).to eq('1w 1d 1h 40m') }
end
end
end
...@@ -946,6 +946,18 @@ describe SystemNoteService do ...@@ -946,6 +946,18 @@ describe SystemNoteService do
expect(subject.note).to eq "changed time estimate to 1w 4d 5h" expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end end
context 'when time_tracking_limit_to_hours setting is true' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
end
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 77h"
end
end
end end
context 'without a time estimate' do context 'without a time estimate' do
...@@ -1022,6 +1034,18 @@ describe SystemNoteService do ...@@ -1022,6 +1034,18 @@ describe SystemNoteService do
end end
end end
context 'when time_tracking_limit_to_hours setting is true' do
before do
stub_application_setting(time_tracking_limit_to_hours: true)
end
it 'sets the note text' do
spend_time!(277200)
expect(subject.note).to eq "added 77h of time spent"
end
end
def spend_time!(seconds) def spend_time!(seconds)
noteable.spend_time(duration: seconds, user_id: author.id) noteable.spend_time(duration: seconds, user_id: author.id)
noteable.save! noteable.save!
......
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