Commit ed8605c4 authored by Phil Hughes's avatar Phil Hughes

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

Limit time tracking units to hours

Closes #30355

See merge request gitlab-org/gitlab-ce!29469
parents fc5475b4 275a1758
...@@ -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 }));
}, },
}, },
}; };
......
...@@ -49,6 +49,7 @@ export default () => { ...@@ -49,6 +49,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.integer "custom_project_templates_group_id" t.integer "custom_project_templates_group_id"
t.boolean "elasticsearch_limit_indexing", default: false, null: false t.boolean "elasticsearch_limit_indexing", default: false, null: false
t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0" t.string "geo_node_allowed_ips", default: "0.0.0.0/0, ::/0"
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
......
...@@ -231,6 +231,7 @@ are listed in the descriptions of the relevant settings. ...@@ -231,6 +231,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
...@@ -5857,6 +5857,9 @@ msgstr "" ...@@ -5857,6 +5857,9 @@ msgstr ""
msgid "Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}." msgid "Let's Encrypt is a free, automated, and open certificate authority (CA) that gives digital certificates in order to enable HTTPS (SSL/TLS) for websites. Learn more about Let's Encrypt configuration by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}."
msgstr "" msgstr ""
msgid "Limit display of time tracking units to hours."
msgstr ""
msgid "Limited to showing %d event at most" msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most" msgid_plural "Limited to showing %d events at most"
msgstr[0] "" msgstr[0] ""
......
...@@ -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