Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
李少辉-开发者
gitlab-foss
提交
64dd41a0
G
gitlab-foss
项目概览
李少辉-开发者
/
gitlab-foss
通知
15
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
G
gitlab-foss
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
64dd41a0
编写于
12月 20, 2016
作者:
B
Bryce Johnson
提交者:
Ruben Davila
1月 15, 2017
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Backport timetracking frontend to CE.
上级
f1bd9f05
变更
22
隐藏空白更改
内联
并排
Showing
22 changed file
with
792 addition
and
16 deletion
+792
-16
app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+0
-2
app/assets/javascripts/issuable/issuable_bundle.js.es6
app/assets/javascripts/issuable/issuable_bundle.js.es6
+1
-0
app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
.../issuable/time_tracking/components/collapsed_state.js.es6
+42
-0
app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
.../issuable/time_tracking/components/comparison_pane.js.es6
+69
-0
app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
...suable/time_tracking/components/estimate_only_pane.js.es6
+13
-0
app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
...ripts/issuable/time_tracking/components/help_state.js.es6
+24
-0
app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
...issuable/time_tracking/components/no_tracking_pane.js.es6
+11
-0
app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
.../issuable/time_tracking/components/spent_only_pane.js.es6
+13
-0
app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
...pts/issuable/time_tracking/components/time_tracker.js.es6
+118
-0
app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
...cripts/issuable/time_tracking/time_tracking_bundle.js.es6
+61
-0
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+2
-1
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+99
-0
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+2
-0
app/views/projects/merge_requests/_show.html.haml
app/views/projects/merge_requests/_show.html.haml
+1
-0
app/views/projects/merge_requests/conflicts.html.haml
app/views/projects/merge_requests/conflicts.html.haml
+1
-0
app/views/shared/icons/_icon_stopwatch.svg
app/views/shared/icons/_icon_stopwatch.svg
+1
-0
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+12
-2
config/application.rb
config/application.rb
+2
-0
spec/features/issues/user_uses_slash_commands_spec.rb
spec/features/issues/user_uses_slash_commands_spec.rb
+26
-0
spec/javascripts/issuable_time_tracker_spec.js.es6
spec/javascripts/issuable_time_tracker_spec.js.es6
+201
-0
spec/javascripts/pretty_time_spec.js.es6
spec/javascripts/pretty_time_spec.js.es6
+11
-11
spec/support/time_tracking_shared_examples.rb
spec/support/time_tracking_shared_examples.rb
+82
-0
未找到文件。
app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
浏览文件 @
64dd41a0
...
...
@@ -2,8 +2,6 @@
/* global Vue */
/* global ResolveCount */
//= require vue
//= require vue-resource
//= require_directory ./models
//= require_directory ./stores
//= require_directory ./services
...
...
app/assets/javascripts/issuable/issuable_bundle.js.es6
0 → 100644
浏览文件 @
64dd41a0
//= require ./time_tracking/time_tracking_bundle
app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
//= require lib/utils/pretty_time
(() => {
Vue.component('time-tracking-collapsed-state', {
name: 'time-tracking-collapsed-state',
props: [
'showComparisonState',
'showSpentOnlyState',
'showEstimateOnlyState',
'showNoTimeTrackingState',
'timeSpentHumanReadable',
'timeEstimateHumanReadable',
'stopwatchSvg',
],
methods: {
abbreviateTime(timeStr) {
return gl.utils.prettyTime.abbreviateTime(timeStr);
},
},
template: `
<div class='sidebar-collapsed-icon'>
<div v-html='stopwatchSvg'></div>
<div class='time-tracking-collapsed-summary'>
<div class='compare' v-if='showComparisonState'>
<span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='estimate-only' v-if='showEstimateOnlyState'>
<span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span>
</div>
<div class='spend-only' v-if='showSpentOnlyState'>
<span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span>
</div>
<div class='no-tracking' v-if='showNoTimeTrackingState'>
<span class='no-value'>None</span>
</div>
</div>
</div>
`,
});
})();
app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
//= require lib/utils/pretty_time
(() => {
const prettyTime = gl.utils.prettyTime;
Vue.component('time-tracking-comparison-pane', {
name: 'time-tracking-comparison-pane',
props: [
'timeSpent',
'timeEstimate',
'timeSpentHumanReadable',
'timeEstimateHumanReadable',
],
computed: {
parsedRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent;
return prettyTime.parseSeconds(diffSeconds);
},
timeRemainingHumanReadable() {
return prettyTime.stringifyTime(this.parsedRemaining);
},
timeRemainingTooltip() {
const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
return `${prefix} ${this.timeRemainingHumanReadable}`;
},
/* Diff values for comparison meter */
timeRemainingMinutes() {
return this.timeEstimate - this.timeSpent;
},
timeRemainingPercent() {
return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`;
},
timeRemainingStatusClass() {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
},
/* Parsed time values */
parsedEstimate() {
return prettyTime.parseSeconds(this.timeEstimate);
},
parsedSpent() {
return prettyTime.parseSeconds(this.timeSpent);
},
},
template: `
<div class='time-tracking-comparison-pane'>
<div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay'
:aria-valuenow='timeRemainingTooltip'
:title='timeRemainingTooltip'
:data-original-title='timeRemainingTooltip'
:class='timeRemainingStatusClass'>
<div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'>
<div :style='{ width: timeRemainingPercent }' class='meter-fill'></div>
</div>
<div class='compare-display-container'>
<div class='compare-display pull-left'>
<span class='compare-label'>Spent</span>
<span class='compare-value spent'>{{ timeSpentHumanReadable }}</span>
</div>
<div class='compare-display estimated pull-right'>
<span class='compare-label'>Est</span>
<span class='compare-value'>{{ timeEstimateHumanReadable }}</span>
</div>
</div>
</div>
</div>
`,
});
})();
app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
(() => {
Vue.component('time-tracking-estimate-only-pane', {
name: 'time-tracking-estimate-only-pane',
props: ['timeEstimateHumanReadable'],
template: `
<div class='time-tracking-estimate-only-pane'>
<span class='bold'>Estimated:</span>
{{ timeEstimateHumanReadable }}
</div>
`,
});
})();
app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
(() => {
Vue.component('time-tracking-help-state', {
name: 'time-tracking-help-state',
props: ['docsUrl'],
template: `
<div class='time-tracking-help-state'>
<div class='time-tracking-info'>
<h4>Track time with slash commands</h4>
<p>Slash commands can be used in the issues description and comment boxes.</p>
<p>
<code>/estimate</code>
will update the estimated time with the latest command.
</p>
<p>
<code>/spend</code>
will update the sum of the time spent.
</p>
<a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a>
</div>
</div>
`,
});
})();
app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
(() => {
Vue.component('time-tracking-no-tracking-pane', {
name: 'time-tracking-no-tracking-pane',
template: `
<div class='time-tracking-no-tracking-pane'>
<span class='no-value'>No estimate or time spent</span>
</div>
`,
});
})();
app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
(() => {
Vue.component('time-tracking-spent-only-pane', {
name: 'time-tracking-spent-only-pane',
props: ['timeSpentHumanReadable'],
template: `
<div class='time-tracking-spend-only-pane'>
<span class='bold'>Spent:</span>
{{ timeSpentHumanReadable }}
</div>
`,
});
})();
app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
//= require ./help_state
//= require ./collapsed_state
//= require ./spent_only_pane
//= require ./no_tracking_pane
//= require ./estimate_only_pane
//= require ./comparison_pane
(() => {
Vue.component('issuable-time-tracker', {
name: 'issuable-time-tracker',
props: [
'time_estimate',
'time_spent',
'human_time_estimate',
'human_time_spent',
'stopwatchSvg',
'docsUrl',
],
data() {
return {
showHelp: false,
};
},
computed: {
timeSpent() {
return this.time_spent;
},
timeEstimate() {
return this.time_estimate;
},
timeEstimateHumanReadable() {
return this.human_time_estimate;
},
timeSpentHumanReadable() {
return this.human_time_spent;
},
hasTimeSpent() {
return !!this.timeSpent;
},
hasTimeEstimate() {
return !!this.timeEstimate;
},
showComparisonState() {
return this.hasTimeEstimate && this.hasTimeSpent;
},
showEstimateOnlyState() {
return this.hasTimeEstimate && !this.hasTimeSpent;
},
showSpentOnlyState() {
return this.hasTimeSpent && !this.hasTimeEstimate;
},
showNoTimeTrackingState() {
return !this.hasTimeEstimate && !this.hasTimeSpent;
},
showHelpState() {
return !!this.showHelp;
},
},
methods: {
toggleHelpState(show) {
this.showHelp = show;
},
},
template: `
<div class='time_tracker time-tracking-component-wrap' v-cloak>
<time-tracking-collapsed-state
:show-comparison-state='showComparisonState'
:show-help-state='showHelpState'
:show-spent-only-state='showSpentOnlyState'
:show-estimate-only-state='showEstimateOnlyState'
:time-spent-human-readable='timeSpentHumanReadable'
:time-estimate-human-readable='timeEstimateHumanReadable'
:stopwatch-svg='stopwatchSvg'>
</time-tracking-collapsed-state>
<div class='title hide-collapsed'>
Time tracking
<div class='help-button pull-right'
v-if='!showHelpState'
@click='toggleHelpState(true)'>
<i class='fa fa-question-circle'></i>
</div>
<div class='close-help-button pull-right'
v-if='showHelpState'
@click='toggleHelpState(false)'>
<i class='fa fa-close'></i>
</div>
</div>
<div class='time-tracking-content hide-collapsed'>
<time-tracking-estimate-only-pane
v-if='showEstimateOnlyState'
:time-estimate-human-readable='timeEstimateHumanReadable'>
</time-tracking-estimate-only-pane>
<time-tracking-spent-only-pane
v-if='showSpentOnlyState'
:time-spent-human-readable='timeSpentHumanReadable'>
</time-tracking-spent-only-pane>
<time-tracking-no-tracking-pane
v-if='showNoTimeTrackingState'>
</time-tracking-no-tracking-pane>
<time-tracking-comparison-pane
v-if='showComparisonState'
:time-estimate='timeEstimate'
:time-spent='timeSpent'
:time-spent-human-readable='timeSpentHumanReadable'
:time-estimate-human-readable='timeEstimateHumanReadable'>
</time-tracking-comparison-pane>
<transition name='help-state-toggle'>
<time-tracking-help-state
v-if='showHelpState'
:docs-url='docsUrl'>
</time-tracking-help-state>
</transition>
</div>
</div>
`,
});
})();
app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* global Vue */
//= require ./components/time_tracker
//= require smart_interval
//= require subbable_resource
(() => {
/* This Vue instance represents what will become the parent instance for the
* sidebar. It will be responsible for managing `issuable` state and propagating
* changes to sidebar components. We will want to create a separate service to
* interface with the server at that point.
*/
class IssuableTimeTracking {
constructor(issuableJSON) {
const parsedIssuable = JSON.parse(issuableJSON);
return this.initComponent(parsedIssuable);
}
initComponent(parsedIssuable) {
this.parentInstance = new Vue({
el: '#issuable-time-tracker',
data: {
issuable: parsedIssuable,
},
methods: {
fetchIssuable() {
return gl.IssuableResource.get.call(gl.IssuableResource, {
type: 'GET',
url: gl.IssuableResource.endpoint,
});
},
updateState(data) {
this.issuable = data;
},
subscribeToUpdates() {
gl.IssuableResource.subscribe(data => this.updateState(data));
},
listenForSlashCommands() {
$(document).on('ajax:success', '.gfm-form', (e, data) => {
const subscribedCommands = ['spend_time', 'time_estimate'];
const changedCommands = data.commands_changes;
if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) {
this.fetchIssuable();
}
});
},
},
created() {
this.fetchIssuable();
},
mounted() {
this.subscribeToUpdates();
this.listenForSlashCommands();
},
});
}
}
gl.IssuableTimeTracking = IssuableTimeTracking;
})(window.gl || (window.gl = {}));
app/assets/stylesheets/framework/variables.scss
浏览文件 @
64dd41a0
...
...
@@ -5,7 +5,7 @@ $sidebar_collapsed_width: 62px;
$sidebar_width
:
220px
;
$gutter_collapsed_width
:
62px
;
$gutter_width
:
290px
;
$gutter_inner_width
:
25
8
px
;
$gutter_inner_width
:
25
0
px
;
$sidebar-transition-duration
:
.15s
;
$sidebar-breakpoint
:
1024px
;
...
...
@@ -85,6 +85,7 @@ $warning-message-border: #f0e2bb;
*/
$border-color
:
#e5e5e5
;
$focus-border-color
:
#3aabf0
;
$sidebar-collapsed-icon-color
:
#999
;
$well-expand-item
:
#e8f2f7
;
$well-inner-border
:
#eef0f2
;
$well-light-border
:
#f1f1f1
;
...
...
app/assets/stylesheets/pages/issuable.scss
浏览文件 @
64dd41a0
...
...
@@ -469,3 +469,102 @@
}
}
}
.time_tracker
{
padding-bottom
:
0
;
border-bottom
:
0
;
.sidebar-collapsed-icon
{
>
.stopwatch-svg
{
display
:
inline-block
;
}
svg
{
width
:
16px
;
height
:
16px
;
fill
:
$sidebar-collapsed-icon-color
;
}
&
:hover
svg
{
fill
:
$gl-gray
;
}
}
.help-button
,
.close-help-button
{
cursor
:
pointer
;
}
.compare-meter
{
&
.within_estimate
{
.meter-fill
{
background
:
$gl-primary
;
}
}
&
.over_estimate
{
.meter-fill
{
background
:
$red-light
;
}
.time-remaining
,
.compare-value.spent
{
color
:
$red-light
;
}
}
}
.meter-container
{
background
:
$border-gray-light
;
border-radius
:
3px
;
.meter-fill
{
max-width
:
100%
;
height
:
5px
;
border-radius
:
3px
;
background
:
$gl-primary
;
}
}
.compare-display-container
{
display
:
flex
;
justify-content
:
space-between
;
margin-top
:
5px
;
.compare-display
{
font-size
:
13px
;
color
:
$gl-gray-light
;
.compare-value
{
color
:
$gl-gray
;
}
}
}
.time-tracking-help-state
{
background
:
$white-light
;
margin
:
16px
-20px
0
;
padding
:
16px
20px
;
border-top
:
1px
solid
$border-gray-light
;
border-bottom
:
1px
solid
$border-gray-light
;
a
:hover
{
color
:
$btn-white-active
;
}
}
.help-state-toggle-enter-active
{
transition
:
all
.8s
ease
;
}
.help-state-toggle-leave-active
{
transition
:
all
.5s
ease
;
}
.help-state-toggle-enter
,
.help-state-toggle-leave-active
{
opacity
:
0
;
}
}
app/views/projects/issues/show.html.haml
浏览文件 @
64dd41a0
...
...
@@ -2,6 +2,8 @@
-
page_title
"
#{
@issue
.
title
}
(
#{
@issue
.
to_reference
}
)"
,
"Issues"
-
page_description
@issue
.
description
-
page_card_attributes
@issue
.
card_attributes
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_tag
(
'lib/vue_resource.js'
)
.clearfix.detail-page-header
.issuable-header
...
...
app/views/projects/merge_requests/_show.html.haml
浏览文件 @
64dd41a0
...
...
@@ -3,6 +3,7 @@
-
page_description
@merge_request
.
description
-
page_card_attributes
@merge_request
.
card_attributes
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_tag
(
'lib/vue_resource.js'
)
=
page_specific_javascript_tag
(
'diff_notes/diff_notes_bundle.js'
)
.merge-request
{
'data-url'
=>
merge_request_path
(
@merge_request
)
}
...
...
app/views/projects/merge_requests/conflicts.html.haml
浏览文件 @
64dd41a0
-
page_title
"Merge Conflicts"
,
"
#{
@merge_request
.
title
}
(
#{
@merge_request
.
to_reference
}
"
,
"Merge Requests"
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_tag
(
'lib/vue_resource.js'
)
=
page_specific_javascript_tag
(
'merge_conflicts/merge_conflicts_bundle.js'
)
=
page_specific_javascript_tag
(
'lib/ace.js'
)
=
render
"projects/merge_requests/show/mr_title"
...
...
app/views/shared/icons/_icon_stopwatch.svg
0 → 100644
浏览文件 @
64dd41a0
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 12 14"
enable-background=
"new 0 0 12 14"
><path
d=
"m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"
/><path
d=
"m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"
/><path
d=
"m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"
/></svg>
\ No newline at end of file
app/views/shared/issuable/_sidebar.html.haml
浏览文件 @
64dd41a0
-
todo
=
issuable_todo
(
issuable
)
%aside
.right-sidebar
{
class:
sidebar_gutter_collapsed_class
}
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_tag
(
'issuable/issuable_bundle.js'
)
%aside
.right-sidebar
{
class:
sidebar_gutter_collapsed_class
,
'aria-live'
=>
'polite'
}
.issuable-sidebar
-
can_edit_issuable
=
can?
(
current_user
,
:"admin_
#{
issuable
.
to_ability_name
}
"
,
@project
)
.block.issuable-sidebar-header
...
...
@@ -72,7 +74,13 @@
.selectbox.hide-collapsed
=
f
.
hidden_field
'milestone_id'
,
value:
issuable
.
milestone_id
,
id:
nil
=
dropdown_tag
(
'Milestone'
,
options:
{
title:
'Assign milestone'
,
toggle_class:
'js-milestone-select js-extra-options'
,
filter:
true
,
dropdown_class:
'dropdown-menu-selectable'
,
placeholder:
'Search milestones'
,
data:
{
show_no:
true
,
field_name:
"
#{
issuable
.
to_ability_name
}
[milestone_id]"
,
project_id:
@project
.
id
,
issuable_id:
issuable
.
id
,
milestones:
namespace_project_milestones_path
(
@project
.
namespace
,
@project
,
:json
),
ability_name:
issuable
.
to_ability_name
,
issue_update:
issuable_json_path
(
issuable
),
use_id:
true
}})
-
if
issuable
.
has_attribute?
(
:time_estimate
)
#issuable-time-tracker
.block
%issuable-time-tracker
{
':time_estimate'
=>
'issuable.time_estimate'
,
':time_spent'
=>
'issuable.total_time_spent'
,
':human_time_estimate'
=>
'issuable.human_time_estimate'
,
':human_time_spent'
=>
'issuable.human_total_time_spent'
,
'stopwatch-svg'
=>
custom_icon
(
'icon_stopwatch'
),
'docs-url'
=>
help_page_path
(
'workflow/time_tracking.md'
)}
// Fallback while content is loading
.title.hide-collapsed
Time tracking
=
icon
(
'spinner spin'
)
-
if
issuable
.
has_attribute?
(
:due_date
)
.block.due_date
.sidebar-collapsed-icon
...
...
@@ -162,6 +170,8 @@
=
clipboard_button
(
clipboard_text:
project_ref
,
title:
"Copy reference to clipboard"
,
placement:
"left"
)
:javascript
gl
.
IssuableResource
=
new
gl
.
SubbableResource
(
'
#{
issuable_json_path
(
issuable
)
}
'
);
new
gl
.
IssuableTimeTracking
(
"
#{
escape_javascript
(
serialize_issuable
(
issuable
))
}
"
);
new
MilestoneSelect
(
'
{"namespace":"
#{
@project
.
namespace
.
path
}
","path":"
#{
@project
.
path
}
"}
'
);
new
LabelsSelect
();
new
IssuableContext
(
'
#{
escape_javascript
(
current_user
.
to_json
(
only:
[
:username
,
:id
,
:name
]))
}
'
);
...
...
config/application.rb
浏览文件 @
64dd41a0
...
...
@@ -88,6 +88,7 @@ module Gitlab
config
.
assets
.
precompile
<<
"print.css"
config
.
assets
.
precompile
<<
"notify.css"
config
.
assets
.
precompile
<<
"mailers/*.css"
config
.
assets
.
precompile
<<
"lib/vue_resource.js"
config
.
assets
.
precompile
<<
"katex.css"
config
.
assets
.
precompile
<<
"katex.js"
config
.
assets
.
precompile
<<
"xterm/xterm.css"
...
...
@@ -98,6 +99,7 @@ module Gitlab
config
.
assets
.
precompile
<<
"protected_branches/protected_branches_bundle.js"
config
.
assets
.
precompile
<<
"diff_notes/diff_notes_bundle.js"
config
.
assets
.
precompile
<<
"merge_request_widget/ci_bundle.js"
config
.
assets
.
precompile
<<
"issuable/issuable_bundle.js"
config
.
assets
.
precompile
<<
"boards/boards_bundle.js"
config
.
assets
.
precompile
<<
"cycle_analytics/cycle_analytics_bundle.js"
config
.
assets
.
precompile
<<
"merge_conflicts/merge_conflicts_bundle.js"
...
...
spec/features/issues/user_uses_slash_commands_spec.rb
浏览文件 @
64dd41a0
...
...
@@ -100,6 +100,32 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
end
end
describe
'Issuable time tracking'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
before
do
project
.
team
<<
[
user
,
:developer
]
end
context
'Issue'
do
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
end
it_behaves_like
'issuable time tracker'
end
context
'Merge Request'
do
let
(
:merge_request
)
{
create
(
:merge_request
,
source_project:
project
)
}
before
do
visit
namespace_project_merge_request_path
(
project
.
namespace
,
project
,
merge_request
)
end
it_behaves_like
'issuable time tracker'
end
end
describe
'toggling the WIP prefix from the title from note'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
...
...
spec/javascripts/issuable_time_tracker_spec.js.es6
0 → 100644
浏览文件 @
64dd41a0
/* eslint-disable */
//= require jquery
//= require vue
//= require issuable/time_tracking/components/time_tracker
function initTimeTrackingComponent(opts) {
fixture.set(`
<div>
<div id="mock-container"></div>
</div>
`);
this.initialData = {
time_estimate: opts.timeEstimate,
time_spent: opts.timeSpent,
human_time_estimate: opts.timeEstimateHumanReadable,
human_time_spent: opts.timeSpentHumanReadable,
docsUrl: '/help/workflow/time_tracking.md',
};
const TimeTrackingComponent = Vue.component('issuable-time-tracker');
this.timeTracker = new TimeTrackingComponent({
el: '#mock-container',
propsData: this.initialData,
});
}
((gl) => {
describe('Issuable Time Tracker', function() {
describe('Initialization', function() {
beforeEach(function() {
initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
});
it('should return something defined', function() {
expect(this.timeTracker).toBeDefined();
});
it ('should correctly set timeEstimate', function(done) {
Vue.nextTick(() => {
expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate);
done();
});
});
it ('should correctly set time_spent', function(done) {
Vue.nextTick(() => {
expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent);
done();
});
});
});
describe('Content Display', function() {
describe('Panes', function() {
describe('Comparison pane', function() {
beforeEach(function() {
initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' });
});
it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) {
Vue.nextTick(() => {
const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane');
expect(this.timeTracker.showComparisonState).toBe(true);
done();
});
});
describe('Remaining meter', function() {
it('should display the remaining meter with the correct width', function(done) {
Vue.nextTick(() => {
const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width;
const correctWidth = '5%';
expect(meterWidth).toBe(correctWidth);
done();
})
});
it('should display the remaining meter with the correct background color when within estimate', function(done) {
Vue.nextTick(() => {
const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill');
expect(styledMeter.length).toBe(1);
done()
});
});
it('should display the remaining meter with the correct background color when over estimate', function(done) {
this.timeTracker.time_estimate = 100000;
this.timeTracker.time_spent = 20000000;
Vue.nextTick(() => {
const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill');
expect(styledMeter.length).toBe(1);
done();
});
});
});
});
describe("Estimate only pane", function() {
beforeEach(function() {
initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' });
});
it('should display the human readable version of time estimated', function(done) {
Vue.nextTick(() => {
const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText;
const correctText = 'Estimated: 2h 46m';
expect(estimateText).toBe(correctText);
done();
});
});
});
describe('Spent only pane', function() {
beforeEach(function() {
initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' });
});
it('should display the human readable version of time spent', function(done) {
Vue.nextTick(() => {
const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText;
const correctText = 'Spent: 1h 23m';
expect(spentText).toBe(correctText);
done();
});
});
});
describe('No time tracking pane', function() {
beforeEach(function() {
initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 });
});
it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) {
Vue.nextTick(() => {
const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane');
const noTrackingText =$noTrackingPane.innerText;
const correctText = 'No estimate or time spent';
expect(this.timeTracker.showNoTimeTrackingState).toBe(true);
expect($noTrackingPane).toBeVisible();
expect(noTrackingText).toBe(correctText);
done();
});
});
});
describe("Help pane", function() {
beforeEach(function() {
initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 });
});
it('should not show the "Help" pane by default', function(done) {
Vue.nextTick(() => {
const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
expect(this.timeTracker.showHelpState).toBe(false);
expect($helpPane).toBeNull();
done();
});
});
it('should show the "Help" pane when help button is clicked', function(done) {
Vue.nextTick(() => {
$(this.timeTracker.$el).find('.help-button').click();
setTimeout(() => {
const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
expect(this.timeTracker.showHelpState).toBe(true);
expect($helpPane).toBeVisible();
done();
}, 10);
});
});
it('should not show the "Help" pane when help button is clicked and then closed', function(done) {
Vue.nextTick(() => {
$(this.timeTracker.$el).find('.help-button').click();
setTimeout(() => {
$(this.timeTracker.$el).find('.close-help-button').click();
setTimeout(() => {
const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state');
expect(this.timeTracker.showHelpState).toBe(false);
expect($helpPane).toBeNull();
done();
}, 1000);
}, 1000);
});
});
});
});
});
});
})(window.gl || (window.gl = {}));
spec/javascripts/pretty_time_spec.js.es6
浏览文件 @
64dd41a0
//= require lib/utils/pretty_time
(() => {
const
PrettyTime = gl.P
rettyTime;
const
prettyTime = gl.utils.p
rettyTime;
describe('
P
rettyTime methods', function () {
describe('
p
rettyTime methods', function () {
describe('parseSeconds', function () {
it('should correctly parse a negative value', function () {
const parser =
P
rettyTime.parseSeconds;
const parser =
p
rettyTime.parseSeconds;
const zeroSeconds = parser(-1000);
...
...
@@ -17,7 +17,7 @@
});
it('should correctly parse a zero value', function () {
const parser =
P
rettyTime.parseSeconds;
const parser =
p
rettyTime.parseSeconds;
const zeroSeconds = parser(0);
...
...
@@ -28,7 +28,7 @@
});
it('should correctly parse a small non-zero second values', function () {
const parser =
P
rettyTime.parseSeconds;
const parser =
p
rettyTime.parseSeconds;
const subOneMinute = parser(10);
...
...
@@ -53,7 +53,7 @@
});
it('should correctly parse large second values', function () {
const parser =
P
rettyTime.parseSeconds;
const parser =
p
rettyTime.parseSeconds;
const aboveOneHour = parser(4800);
...
...
@@ -87,7 +87,7 @@
minutes: 20,
};
const timeString =
P
rettyTime.stringifyTime(timeObject);
const timeString =
p
rettyTime.stringifyTime(timeObject);
expect(timeString).toBe('1w 4d 7h 20m');
});
...
...
@@ -100,7 +100,7 @@
minutes: 20,
};
const timeString =
P
rettyTime.stringifyTime(timeObject);
const timeString =
p
rettyTime.stringifyTime(timeObject);
expect(timeString).toBe('4d 20m');
});
...
...
@@ -113,7 +113,7 @@
minutes: 0,
};
const timeString =
P
rettyTime.stringifyTime(timeObject);
const timeString =
p
rettyTime.stringifyTime(timeObject);
expect(timeString).toBe('0m');
});
...
...
@@ -122,12 +122,12 @@
describe('abbreviateTime', function () {
it('should abbreviate stringified times for weeks', function () {
const fullTimeString = '1w 3d 4h 5m';
expect(
P
rettyTime.abbreviateTime(fullTimeString)).toBe('1w');
expect(
p
rettyTime.abbreviateTime(fullTimeString)).toBe('1w');
});
it('should abbreviate stringified times for non-weeks', function () {
const fullTimeString = '0w 3d 4h 5m';
expect(
P
rettyTime.abbreviateTime(fullTimeString)).toBe('3d');
expect(
p
rettyTime.abbreviateTime(fullTimeString)).toBe('3d');
});
});
});
...
...
spec/support/time_tracking_shared_examples.rb
0 → 100644
浏览文件 @
64dd41a0
shared_examples
'issuable time tracker'
do
it
'renders the sidebar component empty state'
do
page
.
within
'.time-tracking-no-tracking-pane'
do
expect
(
page
).
to
have_content
'No estimate or time spent'
end
end
it
'updates the sidebar component when estimate is added'
do
submit_time
(
'/estimate 3w 1d 1h'
)
page
.
within
'.time-tracking-estimate-only-pane'
do
expect
(
page
).
to
have_content
'3w 1d 1h'
end
end
it
'updates the sidebar component when spent is added'
do
submit_time
(
'/spend 3w 1d 1h'
)
page
.
within
'.time-tracking-spend-only-pane'
do
expect
(
page
).
to
have_content
'3w 1d 1h'
end
end
it
'shows the comparison when estimate and spent are added'
do
submit_time
(
'/estimate 3w 1d 1h'
)
submit_time
(
'/spend 3w 1d 1h'
)
page
.
within
'.time-tracking-comparison-pane'
do
expect
(
page
).
to
have_content
'3w 1d 1h'
end
end
it
'updates the sidebar component when estimate is removed'
do
submit_time
(
'/estimate 3w 1d 1h'
)
submit_time
(
'/remove_estimate'
)
page
.
within
'#issuable-time-tracker'
do
expect
(
page
).
to
have_content
'No estimate or time spent'
end
end
it
'updates the sidebar component when spent is removed'
do
submit_time
(
'/spend 3w 1d 1h'
)
submit_time
(
'/remove_time_spent'
)
page
.
within
'#issuable-time-tracker'
do
expect
(
page
).
to
have_content
'No estimate or time spent'
end
end
it
'shows the help state when icon is clicked'
do
page
.
within
'#issuable-time-tracker'
do
find
(
'.help-button'
).
click
expect
(
page
).
to
have_content
'Track time with slash commands'
expect
(
page
).
to
have_content
'Learn more'
end
end
it
'hides the help state when close icon is clicked'
do
page
.
within
'#issuable-time-tracker'
do
find
(
'.help-button'
).
click
find
(
'.close-help-button'
).
click
expect
(
page
).
not_to
have_content
'Track time with slash commands'
expect
(
page
).
not_to
have_content
'Learn more'
end
end
it
'displays the correct help url'
do
page
.
within
'#issuable-time-tracker'
do
find
(
'.help-button'
).
click
expect
(
find_link
(
'Learn more'
)[
:href
]).
to
have_content
(
'/help/workflow/time_tracking.md'
)
end
end
end
def
submit_time
(
slash_command
)
fill_in
'note[note]'
,
with:
slash_command
click_button
'Comment'
wait_for_ajax
end
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录