Rename to time_tracking_limit_to_hours
Changes migration and all other places the attribute is used
This commit is contained in:
parent
4e283ee706
commit
275a17589c
32 changed files with 259 additions and 57 deletions
|
@ -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 @@
|
||||||
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,17 +15,17 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
limitToHours: boardsStore.timeTracking.limitToHours,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
title() {
|
||||||
return stringifyTime(
|
return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true);
|
||||||
parseSeconds(this.estimate, { limitToHours: gon.time_tracking_display_hours_only }),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
timeEstimate() {
|
timeEstimate() {
|
||||||
return stringifyTime(
|
return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }));
|
||||||
parseSeconds(this.estimate, { limitToHours: gon.time_tracking_display_hours_only })
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
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 = {
|
||||||
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);
|
||||||
|
|
|
@ -485,6 +485,7 @@ export const parseSeconds = (
|
||||||
) => {
|
) => {
|
||||||
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;
|
||||||
|
@ -496,10 +497,15 @@ export const parseSeconds = (
|
||||||
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 (limitToHours && minutesPerPeriod > MINUTES_PER_HOUR) {
|
if (minutesPerPeriod === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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, { limitToHours: gon.time_tracking_display_hours_only });
|
return parseSeconds(diffSeconds, { limitToHours: this.limitToHours });
|
||||||
},
|
},
|
||||||
timeRemainingHumanReadable() {
|
timeRemainingHumanReadable() {
|
||||||
return stringifyTime(this.parsedTimeRemaining);
|
return stringifyTime(this.parsedTimeRemaining);
|
||||||
|
|
|
@ -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 {
|
||||||
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 {
|
||||||
: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" />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
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 {
|
||||||
|
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
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,7 +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_display_hours_only,
|
: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
|
||||||
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
|
||||||
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,7 +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_display_hours_only: false,
|
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,
|
||||||
|
|
|
@ -9,9 +9,10 @@
|
||||||
= _('Default first day of the week in calendars and date pickers.')
|
= _('Default first day of the week in calendars and date pickers.')
|
||||||
|
|
||||||
.form-group
|
.form-group
|
||||||
|
= f.label :time_tracking, _('Time tracking'), class: 'label-bold'
|
||||||
.form-check
|
.form-check
|
||||||
= f.check_box :time_tracking_display_hours_only, class: 'form-check-input'
|
= f.check_box :time_tracking_limit_to_hours, class: 'form-check-input'
|
||||||
= f.label :time_tracking_display_hours_only, class: 'form-check-label' do
|
= f.label :time_tracking_limit_to_hours, class: 'form-check-label' do
|
||||||
_('Limit time tracking display to hours.')
|
= _('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 @@
|
||||||
":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 @@
|
||||||
= 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')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
title: Add option to show time tracking values in hours only
|
title: Add option to limit time tracking units to hours
|
||||||
merge_request: 29469
|
merge_request: 29469
|
||||||
author: Jon Kolb
|
author: Jon Kolb
|
||||||
type: added
|
type: added
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
|
||||||
# for more information on how to write migrations for GitLab.
|
# for more information on how to write migrations for GitLab.
|
||||||
|
|
||||||
class AddTimeTrackingDisplayHoursOnlyToApplicationSettings < ActiveRecord::Migration[5.1]
|
class AddTimeTrackingLimitToHoursToApplicationSettings < ActiveRecord::Migration[5.1]
|
||||||
include Gitlab::Database::MigrationHelpers
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
# Set this constant to true if this migration requires downtime.
|
# Set this constant to true if this migration requires downtime.
|
||||||
|
@ -12,10 +12,10 @@ class AddTimeTrackingDisplayHoursOnlyToApplicationSettings < ActiveRecord::Migra
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
add_column_with_default :application_settings, :time_tracking_display_hours_only, :boolean, default: false, allow_null: false
|
add_column_with_default :application_settings, :time_tracking_limit_to_hours, :boolean, default: false, allow_null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
def down
|
def down
|
||||||
remove_column :application_settings, :time_tracking_display_hours_only
|
remove_column :application_settings, :time_tracking_limit_to_hours
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -229,7 +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_display_hours_only", 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
|
||||||
|
|
|
@ -231,7 +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_display_hours_only` | boolean | no | Limit time tracking values to hours only. Default is `false`. |
|
| `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:
|
||||||
|
|
||||||
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/)
|
||||||
|
|
||||||
|
|
|
@ -32,8 +32,6 @@ module Gitlab
|
||||||
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
|
gon.first_day_of_week = current_user&.first_day_of_week || Gitlab::CurrentSettings.first_day_of_week
|
||||||
gon.ee = Gitlab.ee?
|
gon.ee = Gitlab.ee?
|
||||||
|
|
||||||
gon.time_tracking_display_hours_only = Gitlab::CurrentSettings.time_tracking_display_hours_only
|
|
||||||
|
|
||||||
if current_user
|
if current_user
|
||||||
gon.current_user_id = current_user.id
|
gon.current_user_id = current_user.id
|
||||||
gon.current_username = current_user.username
|
gon.current_username = current_user.username
|
||||||
|
|
|
@ -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,11 +16,12 @@ module Gitlab
|
||||||
|
|
||||||
def output(seconds)
|
def output(seconds)
|
||||||
with_custom_config do
|
with_custom_config do
|
||||||
limit_to_hours = Gitlab::CurrentSettings.time_tracking_display_hours_only
|
ChronicDuration.output(seconds, format: :short, limit_to_hours: limit_to_hours_setting, weeks: true) rescue nil
|
||||||
ChronicDuration.output(seconds, format: :short, limit_to_hours: limit_to_hours, 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
|
||||||
|
@ -33,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
|
||||||
|
|
|
@ -5839,6 +5839,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
|
||||||
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
|
||||||
|
|
||||||
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
|
||||||
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
|
||||||
|
|
||||||
page.within('.time-tracking') do
|
context 'when time_tracking_limit_to_hours is true' do
|
||||||
expect(find('.time-tracking-content .compare-meter')['data-original-title']).to eq('Time remaining: 4h')
|
let(:application_settings) { { time_tracking_limit_to_hours: true } }
|
||||||
|
|
||||||
|
it 'shows time tracking progress bar' do
|
||||||
|
expect(compare_meter_tooltip).to eq('Time remaining: 31h 46m')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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', () => {
|
||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,40 +1,70 @@
|
||||||
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>';
|
||||||
|
|
||||||
|
vm.$nextTick(() => {
|
||||||
|
expect(alertSpy).not.toHaveBeenCalled();
|
||||||
|
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
|
||||||
|
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders expanded time estimate in tooltip', () => {
|
describe('when limitToHours is true', () => {
|
||||||
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
|
beforeEach(() => {
|
||||||
'2 weeks 3 days 1 minute',
|
boardsStore.timeTracking.limitToHours = true;
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('prevents tooltip xss', done => {
|
const Component = Vue.extend(IssueTimeEstimate);
|
||||||
const alertSpy = spyOn(window, 'alert');
|
vm = mountComponent(Component, {
|
||||||
vm.estimate = 'Foo <script>alert("XSS")</script>';
|
estimate: 374460,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
vm.$nextTick(() => {
|
it('renders the correct time estimate', () => {
|
||||||
expect(alertSpy).not.toHaveBeenCalled();
|
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('104h 1m');
|
||||||
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
|
});
|
||||||
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
|
|
||||||
done();
|
it('renders expanded time estimate in tooltip', () => {
|
||||||
|
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
|
||||||
|
'104 hours 1 minute',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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', () => {
|
||||||
timeSpent,
|
timeSpent,
|
||||||
humanTimeEstimate: timeEstimateHumanReadable,
|
humanTimeEstimate: timeEstimateHumanReadable,
|
||||||
humanTimeSpent: timeSpentHumanReadable,
|
humanTimeSpent: timeSpentHumanReadable,
|
||||||
|
limitToHours: Boolean(limitToHours),
|
||||||
rootPath: '/',
|
rootPath: '/',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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({
|
||||||
|
|
43
spec/lib/gitlab/time_tracking_formatter_spec.rb
Normal file
43
spec/lib/gitlab/time_tracking_formatter_spec.rb
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
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
|
||||||
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!
|
||||||
|
|
Loading…
Reference in a new issue