BigW Consortium Gitlab
Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
G
gitlab-ce
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Forest Godfrey
gitlab-ce
Commits
49b85262
Commit
49b85262
authored
Apr 19, 2018
by
Dennis Tang
Committed by
Rémy Coutable
Apr 19, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Resolve "Improve tooltips of collapsed sidebars"
parent
79905b5a
Hide whitespace changes
Inline
Side-by-side
Showing
36 changed files
with
458 additions
and
170 deletions
+458
-170
due_date_select.js
app/assets/javascripts/due_date_select.js
+8
-1
labels_select.js
app/assets/javascripts/labels_select.js
+2
-3
milestone_select.js
app/assets/javascripts/milestone_select.js
+10
-4
right_sidebar.js
app/assets/javascripts/right_sidebar.js
+9
-3
assignees.vue
...ts/javascripts/sidebar/components/assignees/assignees.vue
+19
-1
sidebar_assignees.vue
...cripts/sidebar/components/assignees/sidebar_assignees.vue
+9
-3
confidential_issue_sidebar.vue
...ar/components/confidential/confidential_issue_sidebar.vue
+15
-4
lock_issue_sidebar.vue
...avascripts/sidebar/components/lock/lock_issue_sidebar.vue
+18
-3
participants.vue
...ascripts/sidebar/components/participants/participants.vue
+14
-4
collapsed_state.vue
...ipts/sidebar/components/time_tracking/collapsed_state.vue
+29
-3
mount_sidebar.js
app/assets/javascripts/sidebar/mount_sidebar.js
+1
-0
users_select.js
app/assets/javascripts/users_select.js
+5
-2
toggle_sidebar.vue
...ascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+27
-12
variables.scss
app/assets/stylesheets/framework/variables.scss
+3
-0
issuable.scss
app/assets/stylesheets/pages/issuable.scss
+25
-3
milestone.scss
app/assets/stylesheets/pages/milestone.scss
+10
-4
issuables_helper.rb
app/helpers/issuables_helper.rb
+34
-4
milestones_helper.rb
app/helpers/milestones_helper.rb
+74
-17
milestoneish.rb
app/models/concerns/milestoneish.rb
+2
-2
entity_date_helper.rb
app/serializers/entity_date_helper.rb
+27
-0
_issue.html.haml
app/views/projects/issues/_issue.html.haml
+1
-1
_merge_request.html.haml
app/views/projects/merge_requests/_merge_request.html.haml
+1
-1
_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+5
-6
_sidebar_assignees.html.haml
app/views/shared/issuable/_sidebar_assignees.html.haml
+1
-1
_sidebar_todo.html.haml
app/views/shared/issuable/_sidebar_todo.html.haml
+3
-3
_merge_request_assignee.html.haml
...ws/shared/issuable/form/_merge_request_assignee.html.haml
+1
-1
_sidebar.html.haml
app/views/shared/milestones/_sidebar.html.haml
+21
-14
25010-collapsed-sidebar-tooltips.yml
changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
+5
-0
issues_functionalities.md
doc/user/project/issues/issues_functionalities.md
+1
-1
todos.md
doc/workflow/todos.md
+2
-2
todo_spec.rb
spec/features/issues/todo_spec.rb
+2
-2
issuables_helper_spec.rb
spec/helpers/issuables_helper_spec.rb
+6
-2
milestones_helper_spec.rb
spec/helpers/milestones_helper_spec.rb
+0
-54
collapsed_sidebar_todo_spec.js
spec/javascripts/collapsed_sidebar_todo_spec.js
+5
-5
milestone_spec.rb
spec/models/milestone_spec.rb
+8
-4
entity_date_helper_spec.rb
spec/serializers/entity_date_helper_spec.rb
+55
-0
No files found.
app/assets/javascripts/due_date_select.js
View file @
49b85262
...
...
@@ -2,7 +2,9 @@
import
$
from
'jquery'
;
import
Pikaday
from
'pikaday'
;
import
{
__
}
from
'~/locale'
;
import
axios
from
'./lib/utils/axios_utils'
;
import
{
timeFor
}
from
'./lib/utils/datetime_utility'
;
import
{
parsePikadayDate
,
pikadayToString
}
from
'./lib/utils/datefix'
;
class
DueDateSelect
{
...
...
@@ -14,6 +16,7 @@ class DueDateSelect {
this
.
$dropdownParent
=
$dropdownParent
;
this
.
$datePicker
=
$dropdownParent
.
find
(
'.js-due-date-calendar'
);
this
.
$block
=
$block
;
this
.
$sidebarCollapsedValue
=
$block
.
find
(
'.sidebar-collapsed-icon'
);
this
.
$selectbox
=
$dropdown
.
closest
(
'.selectbox'
);
this
.
$value
=
$block
.
find
(
'.value'
);
this
.
$valueContent
=
$block
.
find
(
'.value-content'
);
...
...
@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate
(
isDropdown
)
{
const
selectedDateValue
=
this
.
datePayload
[
this
.
abilityName
].
due_date
;
const
displayedDateStyle
=
this
.
displayedDate
!==
'No due date'
?
'bold'
:
'no-value'
;
const
hasDueDate
=
this
.
displayedDate
!==
'No due date'
;
const
displayedDateStyle
=
hasDueDate
?
'bold'
:
'no-value'
;
this
.
$loading
.
removeClass
(
'hidden'
).
fadeIn
();
...
...
@@ -145,10 +149,13 @@ class DueDateSelect {
return
axios
.
put
(
this
.
issueUpdateURL
,
this
.
datePayload
)
.
then
(()
=>
{
const
tooltipText
=
hasDueDate
?
`
${
__
(
'Due date'
)}
<br />
${
selectedDateValue
}
(
${
timeFor
(
selectedDateValue
)}
)`
:
__
(
'Due date'
);
if
(
isDropdown
)
{
this
.
$dropdown
.
trigger
(
'loaded.gl.dropdown'
);
this
.
$dropdown
.
dropdown
(
'toggle'
);
}
this
.
$sidebarCollapsedValue
.
attr
(
'data-original-title'
,
tooltipText
);
return
this
.
$loading
.
fadeOut
();
});
}
...
...
app/assets/javascripts/labels_select.js
View file @
49b85262
...
...
@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown
.
trigger
(
'loading.gl.dropdown'
);
axios
.
put
(
issueUpdateURL
,
data
)
.
then
(({
data
})
=>
{
var
labelCount
,
template
,
labelTooltipTitle
,
labelTitles
;
var
labelCount
,
template
,
labelTooltipTitle
,
labelTitles
,
formattedLabels
;
$loading
.
fadeOut
();
$dropdown
.
trigger
(
'loaded.gl.dropdown'
);
$selectbox
.
hide
();
...
...
@@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle
=
labelTitles
.
join
(
', '
);
}
else
{
labelTooltipTitle
=
''
;
$sidebarLabelTooltip
.
tooltip
(
'destroy'
);
labelTooltipTitle
=
__
(
'Labels'
);
}
$sidebarLabelTooltip
...
...
app/assets/javascripts/milestone_select.js
View file @
49b85262
...
...
@@ -4,6 +4,7 @@
import
$
from
'jquery'
;
import
_
from
'underscore'
;
import
{
__
}
from
'~/locale'
;
import
axios
from
'./lib/utils/axios_utils'
;
import
{
timeFor
}
from
'./lib/utils/datetime_utility'
;
import
ModalStore
from
'./boards/stores/modal_store'
;
...
...
@@ -25,7 +26,7 @@ export default class MilestoneSelect {
}
$els
.
each
((
i
,
dropdown
)
=>
{
let
collapsedSidebarLabelTemplate
,
milestoneLinkNoneTemplate
,
milestoneLinkTemplate
,
selectedMilestone
,
selectedMilestoneDefault
;
let
milestoneLinkNoneTemplate
,
milestoneLinkTemplate
,
selectedMilestone
,
selectedMilestoneDefault
;
const
$dropdown
=
$
(
dropdown
);
const
projectId
=
$dropdown
.
data
(
'projectId'
);
const
milestonesUrl
=
$dropdown
.
data
(
'milestones'
);
...
...
@@ -52,7 +53,6 @@ export default class MilestoneSelect {
if
(
issueUpdateURL
)
{
milestoneLinkTemplate
=
_
.
template
(
'<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'
);
milestoneLinkNoneTemplate
=
'<span class="no-value">None</span>'
;
collapsedSidebarLabelTemplate
=
_
.
template
(
'<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>'
);
}
return
$dropdown
.
glDropdown
({
showMenuAbove
:
showMenuAbove
,
...
...
@@ -214,10 +214,16 @@ export default class MilestoneSelect {
data
.
milestone
.
remaining
=
timeFor
(
data
.
milestone
.
due_date
);
data
.
milestone
.
name
=
data
.
milestone
.
title
;
$value
.
html
(
milestoneLinkTemplate
(
data
.
milestone
));
return
$sidebarCollapsedValue
.
find
(
'span'
).
html
(
collapsedSidebarLabelTemplate
(
data
.
milestone
));
return
$sidebarCollapsedValue
.
attr
(
'data-original-title'
,
`
${
data
.
milestone
.
name
}
<br />
${
data
.
milestone
.
remaining
}
`
)
.
find
(
'span'
)
.
text
(
data
.
milestone
.
title
);
}
else
{
$value
.
html
(
milestoneLinkNoneTemplate
);
return
$sidebarCollapsedValue
.
find
(
'span'
).
text
(
'No'
);
return
$sidebarCollapsedValue
.
attr
(
'data-original-title'
,
__
(
'Milestone'
))
.
find
(
'span'
)
.
text
(
__
(
'None'
));
}
})
.
catch
(()
=>
{
...
...
app/assets/javascripts/right_sidebar.js
View file @
49b85262
...
...
@@ -5,6 +5,7 @@ import _ from 'underscore';
import
Cookies
from
'js-cookie'
;
import
flash
from
'./flash'
;
import
axios
from
'./lib/utils/axios_utils'
;
import
{
__
}
from
'./locale'
;
function
Sidebar
(
currentUser
)
{
this
.
toggleTodo
=
this
.
toggleTodo
.
bind
(
this
);
...
...
@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
};
Sidebar
.
prototype
.
sidebarToggleClicked
=
function
(
e
,
triggered
)
{
var
$allGutterToggleIcons
,
$this
,
$thisIcon
;
var
$allGutterToggleIcons
,
$this
,
isExpanded
,
tooltipLabel
;
e
.
preventDefault
();
$this
=
$
(
this
);
$thisIcon
=
$this
.
find
(
'i'
);
isExpanded
=
$this
.
find
(
'i'
).
hasClass
(
'fa-angle-double-right'
);
tooltipLabel
=
isExpanded
?
__
(
'Expand sidebar'
)
:
__
(
'Collapse sidebar'
);
$allGutterToggleIcons
=
$
(
'.js-sidebar-toggle i'
);
if
(
$thisIcon
.
hasClass
(
'fa-angle-double-right'
))
{
if
(
isExpanded
)
{
$allGutterToggleIcons
.
removeClass
(
'fa-angle-double-right'
).
addClass
(
'fa-angle-double-left'
);
$
(
'aside.right-sidebar'
).
removeClass
(
'right-sidebar-expanded'
).
addClass
(
'right-sidebar-collapsed'
);
$
(
'.layout-page'
).
removeClass
(
'right-sidebar-expanded'
).
addClass
(
'right-sidebar-collapsed'
);
...
...
@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if
(
gl
.
lazyLoader
)
gl
.
lazyLoader
.
loadCheck
();
}
$this
.
attr
(
'data-original-title'
,
tooltipLabel
);
if
(
!
triggered
)
{
Cookies
.
set
(
"collapsed_gutter"
,
$
(
'.right-sidebar'
).
hasClass
(
'right-sidebar-collapsed'
));
}
...
...
app/assets/javascripts/sidebar/components/assignees/assignees.vue
View file @
49b85262
<
script
>
import
{
__
}
from
'~/locale'
;
import
tooltip
from
'~/vue_shared/directives/tooltip'
;
export
default
{
name
:
'Assignees'
,
directives
:
{
tooltip
,
},
props
:
{
rootPath
:
{
type
:
String
,
...
...
@@ -14,6 +20,11 @@ export default {
type
:
Boolean
,
required
:
true
,
},
issuableType
:
{
type
:
String
,
require
:
true
,
default
:
'issue'
,
},
},
data
()
{
return
{
...
...
@@ -62,6 +73,12 @@ export default {
names
.
push
(
`+
${
this
.
users
.
length
-
maxRender
}
more`
);
}
if
(
!
this
.
users
.
length
)
{
const
emptyTooltipLabel
=
this
.
issuableType
===
'issue'
?
__
(
'Assignee(s)'
)
:
__
(
'Assignee'
);
names
.
push
(
emptyTooltipLabel
);
}
return
names
.
join
(
', '
);
},
sidebarAvatarCounter
()
{
...
...
@@ -109,7 +126,8 @@ export default {
<div>
<div
class=
"sidebar-collapsed-icon sidebar-collapsed-user"
:class=
"
{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
:class=
"
{ 'multiple-users': hasMoreThanOneAssignee }"
v-tooltip
data-container="body"
data-placement="left"
:title="collapsedTooltipTitle"
...
...
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
View file @
49b85262
<
script
>
import
Flash
from
'../../../flash'
;
import
Flash
from
'~/flash'
;
import
eventHub
from
'~/sidebar/event_hub'
;
import
Store
from
'~/sidebar/stores/sidebar_store'
;
import
AssigneeTitle
from
'./assignee_title.vue'
;
import
Assignees
from
'./assignees.vue'
;
import
Store
from
'../../stores/sidebar_store'
;
import
eventHub
from
'../../event_hub'
;
export
default
{
name
:
'SidebarAssignees'
,
...
...
@@ -25,6 +25,11 @@ export default {
required
:
false
,
default
:
false
,
},
issuableType
:
{
type
:
String
,
require
:
true
,
default
:
'issue'
,
},
},
data
()
{
return
{
...
...
@@ -90,6 +95,7 @@ export default {
:users=
"store.assignees"
:editable=
"store.editable"
@
assign-self=
"assignSelf"
:issuable-type=
"issuableType"
/>
</div>
</
template
>
app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
View file @
49b85262
<
script
>
import
Flash
from
'../../../flash'
;
import
{
__
}
from
'~/locale'
;
import
Flash
from
'~/flash'
;
import
tooltip
from
'~/vue_shared/directives/tooltip'
;
import
Icon
from
'~/vue_shared/components/icon.vue'
;
import
eventHub
from
'~/sidebar/event_hub'
;
import
editForm
from
'./edit_form.vue'
;
import
Icon
from
'../../../vue_shared/components/icon.vue'
;
import
{
__
}
from
'../../../locale'
;
import
eventHub
from
'../../event_hub'
;
export
default
{
components
:
{
editForm
,
Icon
,
},
directives
:
{
tooltip
,
},
props
:
{
isConfidential
:
{
required
:
true
,
...
...
@@ -33,6 +37,9 @@ export default {
confidentialityIcon
()
{
return
this
.
isConfidential
?
'eye-slash'
:
'eye'
;
},
tooltipLabel
()
{
return
this
.
isConfidential
?
__
(
'Confidential'
)
:
__
(
'Not confidential'
);
},
},
created
()
{
eventHub
.
$on
(
'closeConfidentialityForm'
,
this
.
toggleForm
);
...
...
@@ -65,6 +72,10 @@ export default {
<div
class=
"sidebar-collapsed-icon"
@
click=
"toggleForm"
v-tooltip
data-container=
"body"
data-placement=
"left"
:title=
"tooltipLabel"
>
<icon
:name=
"confidentialityIcon"
...
...
app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
View file @
49b85262
<
script
>
import
{
__
}
from
'~/locale'
;
import
Flash
from
'~/flash'
;
import
tooltip
from
'~/vue_shared/directives/tooltip'
;
import
issuableMixin
from
'~/vue_shared/mixins/issuable'
;
import
Icon
from
'~/vue_shared/components/icon.vue'
;
import
eventHub
from
'~/sidebar/event_hub'
;
import
editForm
from
'./edit_form.vue'
;
import
issuableMixin
from
'../../../vue_shared/mixins/issuable'
;
import
Icon
from
'../../../vue_shared/components/icon.vue'
;
import
eventHub
from
'../../event_hub'
;
export
default
{
components
:
{
editForm
,
Icon
,
},
directives
:
{
tooltip
,
},
mixins
:
[
issuableMixin
],
props
:
{
...
...
@@ -44,6 +51,10 @@ export default {
isLockDialogOpen
()
{
return
this
.
mediator
.
store
.
isLockDialogOpen
;
},
tooltipLabel
()
{
return
this
.
isLocked
?
__
(
'Locked'
)
:
__
(
'Unlocked'
);
},
},
created
()
{
...
...
@@ -85,6 +96,10 @@ export default {
<div
class=
"sidebar-collapsed-icon"
@
click=
"toggleForm"
v-tooltip
data-container=
"body"
data-placement=
"left"
:title=
"tooltipLabel"
>
<icon
:name=
"lockIcon"
...
...
app/assets/javascripts/sidebar/components/participants/participants.vue
View file @
49b85262
<
script
>
import
{
__
,
n__
,
sprintf
}
from
'../../../locale'
;
import
loadingIcon
from
'../../../vue_shared/components/loading_icon.vue'
;
import
userAvatarImage
from
'../../../vue_shared/components/user_avatar/user_avatar_image.vue'
;
import
{
__
,
n__
,
sprintf
}
from
'~/locale'
;
import
tooltip
from
'~/vue_shared/directives/tooltip'
;
import
loadingIcon
from
'~/vue_shared/components/loading_icon.vue'
;
import
userAvatarImage
from
'~/vue_shared/components/user_avatar/user_avatar_image.vue'
;
export
default
{
directives
:
{
tooltip
,
},
components
:
{
loadingIcon
,
userAvatarImage
,
...
...
@@ -72,7 +76,13 @@
<
template
>
<div>
<div
class=
"sidebar-collapsed-icon"
>
<div
class=
"sidebar-collapsed-icon"
v-tooltip
data-container=
"body"
data-placement=
"left"
:title=
"participantLabel"
>
<i
class=
"fa fa-users"
aria-hidden=
"true"
...
...
app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
View file @
49b85262
<
script
>
import
icon
from
'../../../vue_shared/components/icon.vue'
;
import
{
abbreviateTime
}
from
'../../../lib/utils/pretty_time'
;
import
{
__
,
sprintf
}
from
'~/locale'
;
import
{
abbreviateTime
}
from
'~/lib/utils/pretty_time'
;
import
icon
from
'~/vue_shared/components/icon.vue'
;
import
tooltip
from
'~/vue_shared/directives/tooltip'
;
export
default
{
name
:
'TimeTrackingCollapsedState'
,
components
:
{
icon
,
},
directives
:
{
tooltip
,
},
props
:
{
showComparisonState
:
{
type
:
Boolean
,
...
...
@@ -79,6 +84,21 @@
return
''
;
},
timeTrackedTooltipText
()
{
let
title
;
if
(
this
.
showComparisonState
)
{
title
=
__
(
'Time remaining'
);
}
else
if
(
this
.
showEstimateOnlyState
)
{
title
=
__
(
'Estimated'
);
}
else
if
(
this
.
showSpentOnlyState
)
{
title
=
__
(
'Time spent'
);
}
return
sprintf
(
'%{title}: %{text}'
,
({
title
,
text
:
this
.
text
}));
},
tooltipText
()
{
return
this
.
showNoTimeTrackingState
?
__
(
'Time tracking'
)
:
this
.
timeTrackedTooltipText
;
},
},
methods
:
{
abbreviateTime
(
timeStr
)
{
...
...
@@ -89,7 +109,13 @@
</
script
>
<
template
>
<div
class=
"sidebar-collapsed-icon"
>
<div
class=
"sidebar-collapsed-icon"
v-tooltip
data-container=
"body"
data-placement=
"left"
:title=
"tooltipText"
>
<icon
name=
"timer"
/>
<div
class=
"time-tracking-collapsed-summary"
>
<div
:class=
"divClass"
>
...
...
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
49b85262
...
...
@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator
,
field
:
el
.
dataset
.
field
,
signedIn
:
el
.
hasAttribute
(
'data-signed-in'
),
issuableType
:
gl
.
utils
.
isInIssuePage
()
?
'issue'
:
'merge_request'
,
},
}),
});
...
...
app/assets/javascripts/users_select.js
View file @
49b85262
...
...
@@ -5,6 +5,7 @@
import
$
from
'jquery'
;
import
_
from
'underscore'
;
import
axios
from
'./lib/utils/axios_utils'
;
import
{
__
}
from
'./locale'
;
import
ModalStore
from
'./boards/stores/modal_store'
;
// TODO: remove eventHub hack after code splitting refactor
...
...
@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return
axios
.
put
(
issueURL
,
data
)
.
then
(({
data
})
=>
{
var
user
;
var
user
,
tooltipTitle
;
$dropdown
.
trigger
(
'loaded.gl.dropdown'
);
$loading
.
fadeOut
();
if
(
data
.
assignee
)
{
...
...
@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username
:
data
.
assignee
.
username
,
avatar
:
data
.
assignee
.
avatar_url
};
tooltipTitle
=
_
.
escape
(
user
.
name
);
}
else
{
user
=
{
name
:
'Unassigned'
,
username
:
''
,
avatar
:
''
};
tooltipTitle
=
__
(
'Assignee'
);
}
$value
.
html
(
assigneeTemplate
(
user
));
$collapsedSidebar
.
attr
(
'title'
,
_
.
escape
(
user
.
name
)
).
tooltip
(
'fixTitle'
);
$collapsedSidebar
.
attr
(
'title'
,
tooltipTitle
).
tooltip
(
'fixTitle'
);
return
$collapsedSidebar
.
html
(
collapsedAssigneeTemplate
(
user
));
});
};
...
...
app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
View file @
49b85262
<
script
>
export
default
{
name
:
'ToggleSidebar'
,
props
:
{
collapsed
:
{
type
:
Boolean
,
required
:
true
,
},
import
{
__
}
from
'~/locale'
;
import
tooltip
from
'~/vue_shared/directives/tooltip'
;
export
default
{
name
:
'ToggleSidebar'
,
directives
:
{
tooltip
,
},
props
:
{
collapsed
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
tooltipLabel
()
{
return
this
.
collapsed
?
__
(
'Expand sidebar'
)
:
__
(
'Collapse sidebar'
);
},
methods
:
{
toggle
()
{
this
.
$emit
(
'toggle'
);
},
},
methods
:
{
toggle
()
{
this
.
$emit
(
'toggle'
);
},
};
},
};
</
script
>
<
template
>
...
...
@@ -20,6 +31,10 @@
type=
"button"
class=
"btn btn-blank gutter-toggle btn-sidebar-action"
@
click=
"toggle"
v-tooltip
data-container=
"body"
data-placement=
"left"
:title=
"tooltipLabel"
>
<i
aria-label=
"toggle collapse"
...
...
app/assets/stylesheets/framework/variables.scss
View file @
49b85262
...
...
@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin
:
5px
;
$issue-status-expired
:
$orange-500
;
$issuable-sidebar-color
:
$gl-text-color-secondary
;
$sidebar-block-hover-color
:
#ebebeb
;
$group-path-color
:
#999
;
$namespace-kind-color
:
#aaa
;
$panel-heading-link-color
:
#777
;
...
...
@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background
:
rgba
(
0
,
0
,
0
,
0
.04
);
$link-hover-background
:
rgba
(
0
,
0
,
0
,
0
.06
);
$inactive-badge-background
:
rgba
(
0
,
0
,
0
,
0
.08
);
$sidebar-toggle-height
:
60px
;
$sidebar-milestone-toggle-bottom-margin
:
10px
;
/*
* Buttons
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
49b85262
...
...
@@ -187,7 +187,12 @@
padding-left
:
10px
;
&
:hover
{
color
:
$gray-darkest
;
color
:
$gl-text-color
;
}
&
:hover
,
&
:focus
{
text-decoration
:
none
;
}
}
...
...
@@ -368,6 +373,14 @@
padding
:
15px
0
0
;
border-bottom
:
0
;
overflow
:
hidden
;
&
:hover
{
background-color
:
$sidebar-block-hover-color
;
}
&
.issuable-sidebar-header
{
padding-top
:
0
;
}
}
.participants
{
...
...
@@ -380,8 +393,17 @@
.gutter-toggle
{
width
:
100%
;
height
:
$sidebar-toggle-height
;
margin-left
:
0
;
padding-left
:
25px
;
padding-left
:
0
;
border-bottom
:
1px
solid
$border-gray-dark
;
}
a
.gutter-toggle
{
display
:
flex
;
justify-content
:
center
;
flex-direction
:
column
;
text-align
:
center
;
}
.sidebar-collapsed-icon
{
...
...
@@ -428,10 +450,10 @@
.btn-clipboard
{
border
:
0
;
background
:
transparent
;
color
:
$issuable-sidebar-color
;
&
:hover
{
background
:
transparent
;
color
:
$gl-text-color
;
}
}
...
...
app/assets/stylesheets/pages/milestone.scss
View file @
49b85262
...
...
@@ -53,10 +53,6 @@
}
.milestone-sidebar
{
.gutter-toggle
{
margin-bottom
:
10px
;
}
.milestone-progress
{
.title
{
padding-top
:
5px
;
...
...
@@ -102,7 +98,17 @@
margin-right
:
0
;
}
.right-sidebar-expanded
&
{
.gutter-toggle
{
margin-bottom
:
$sidebar-milestone-toggle-bottom-margin
;
}
}
.right-sidebar-collapsed
&
{
.milestone-progress
{
padding-top
:
0
;
}
.reference
{
border-top
:
1px
solid
$border-gray-normal
;
}
...
...
app/helpers/issuables_helper.rb
View file @
49b85262
...
...
@@ -9,6 +9,32 @@ module IssuablesHelper
"right-sidebar-
#{
sidebar_gutter_collapsed?
?
'collapsed'
:
'expanded'
}
"
end
def
sidebar_gutter_tooltip_text
sidebar_gutter_collapsed?
?
_
(
'Expand sidebar'
)
:
_
(
'Collapse sidebar'
)
end
def
sidebar_assignee_tooltip_label
(
issuable
)
if
issuable
.
assignee
issuable
.
assignee
.
name
else
issuable
.
allows_multiple_assignees?
?
_
(
'Assignee(s)'
)
:
_
(
'Assignee'
)
end
end
def
sidebar_due_date_tooltip_label
(
issuable
)
if
issuable
.
due_date
"
#{
_
(
'Due date'
)
}
<br />
#{
due_date_remaining_days
(
issuable
)
}
"
else
_
(
'Due date'
)
end
end
def
due_date_remaining_days
(
issuable
)
remaining_days_in_words
=
remaining_days_in_words
(
issuable
)
"
#{
issuable
.
due_date
.
to_s
(
:medium
)
}
(
#{
remaining_days_in_words
}
)"
end
def
multi_label_name
(
current_labels
,
default_label
)
if
current_labels
&&
current_labels
.
any?
title
=
current_labels
.
first
.
try
(
:title
)
...
...
@@ -153,10 +179,14 @@ module IssuablesHelper
def
issuable_labels_tooltip
(
labels
,
limit:
5
)
first
,
last
=
labels
.
partition
.
with_index
{
|
_
,
i
|
i
<
limit
}
label_names
=
first
.
collect
(
&
:name
)
label_names
<<
"and
#{
last
.
size
}
more"
unless
last
.
empty?
if
labels
&&
labels
.
any?
label_names
=
first
.
collect
(
&
:name
)
label_names
<<
"and
#{
last
.
size
}
more"
unless
last
.
empty?
label_names
.
join
(
', '
)
label_names
.
join
(
', '
)
else
_
(
"Labels"
)
end
end
def
issuables_state_counter_text
(
issuable_type
,
state
,
display_count
)
...
...
@@ -321,7 +351,7 @@ module IssuablesHelper
def
issuable_todo_button_data
(
issuable
,
todo
,
is_collapsed
)
{
todo_text:
"Add todo"
,
mark_text:
"Mark done"
,
mark_text:
"Mark
todo as
done"
,
todo_icon:
(
is_collapsed
?
icon
(
'plus-square'
)
:
nil
),
mark_icon:
(
is_collapsed
?
icon
(
'check-square'
,
class:
'todo-undone'
)
:
nil
),
issuable_id:
issuable
.
id
,
...
...
app/helpers/milestones_helper.rb
View file @
49b85262
module
MilestonesHelper
include
EntityDateHelper
def
milestones_filter_path
(
opts
=
{})
if
@project
project_milestones_path
(
@project
,
opts
)
...
...
@@ -72,6 +74,19 @@ module MilestonesHelper
end
end
def
milestone_progress_tooltip_text
(
milestone
)
has_issues
=
milestone
.
total_issues_count
(
current_user
)
>
0
if
has_issues
[
_
(
'Progress'
),
_
(
"%{percent}%% complete"
)
%
{
percent:
milestone
.
percent_complete
(
current_user
)
}
].
join
(
'<br />'
)
else
_
(
'Progress'
)
end
end
def
milestone_progress_bar
(
milestone
)
options
=
{
class:
'progress-bar progress-bar-success'
,
...
...
@@ -95,27 +110,69 @@ module MilestonesHelper
end
def
milestone_tooltip_title
(
milestone
)
if
milestone
.
due_date
[
milestone
.
due_date
.
to_s
(
:medium
),
"(
#{
milestone_remaining_days
(
milestone
)
}
)"
].
join
(
' '
)
if
milestone
"
#{
milestone
.
title
}
<br />
#{
milestone_tooltip_due_date
(
milestone
)
}
"
else
_
(
'Milestone'
)
end
end
def
milestone_remaining_days
(
milestone
)
if
milestone
.
expired?
content_tag
(
:strong
,
'Past due'
)
elsif
milestone
.
upcoming?
content_tag
(
:strong
,
'Upcoming'
)
elsif
milestone
.
due_date
time_ago
=
time_ago_in_words
(
milestone
.
due_date
)
content
=
time_ago
.
gsub
(
/\d+/
)
{
|
match
|
"<strong>
#{
match
}
</strong>"
}
content
.
slice!
(
"about "
)
content
<<
" remaining"
content
.
html_safe
elsif
milestone
.
start_date
&&
milestone
.
start_date
.
past?
days
=
milestone
.
elapsed_days
content
=
content_tag
(
:strong
,
days
)
content
<<
"
#{
'day'
.
pluralize
(
days
)
}
elapsed"
def
milestone_time_for
(
date
,
date_type
)
title
=
date_type
==
:start
?
"Start date"
:
"End date"
if
date
time_ago
=
time_ago_in_words
(
date
)
time_ago
.
slice!
(
"about "
)
time_ago
<<
if
date
.
past?
" ago"
else
" remaining"
end
content
=
[
title
,
"<br />"
,
date
.
to_s
(
:medium
),
"(
#{
time_ago
}
)"
].
join
(
" "
)
content
.
html_safe
else
title
end
end
def
milestone_issues_tooltip_text
(
milestone
)
issues
=
milestone
.
count_issues_by_state
(
current_user
)
return
_
(
"Issues"
)
if
issues
.
empty?
content
=
[]
content
<<
n_
(
"1 open issue"
,
"%d open issues"
,
issues
[
"opened"
])
%
issues
[
"opened"
]
if
issues
[
"opened"
]
content
<<
n_
(
"1 closed issue"
,
"%d closed issues"
,
issues
[
"closed"
])
%
issues
[
"closed"
]
if
issues
[
"closed"
]
content
.
join
(
'<br />'
).
html_safe
end
def
milestone_merge_requests_tooltip_text
(
milestone
)
merge_requests
=
milestone
.
merge_requests
return
_
(
"Merge requests"
)
if
merge_requests
.
empty?
content
=
[]
content
<<
n_
(
"1 open merge request"
,
"%d open merge requests"
,
merge_requests
.
opened
.
count
)
%
merge_requests
.
opened
.
count
if
merge_requests
.
opened
.
any?
content
<<
n_
(
"1 closed merge request"
,
"%d closed merge requests"
,
merge_requests
.
closed
.
count
)
%
merge_requests
.
closed
.
count
if
merge_requests
.
closed
.
any?
content
<<
n_
(
"1 merged merge request"
,
"%d merged merge requests"
,
merge_requests
.
merged
.
count
)
%
merge_requests
.
merged
.
count
if
merge_requests
.
merged
.
any?
content
.
join
(
'<br />'
).
html_safe
end
def
milestone_tooltip_due_date
(
milestone
)
if
milestone
.
due_date
"
#{
milestone
.
due_date
.
to_s
(
:medium
)
}
(
#{
remaining_days_in_words
(
milestone
)
}
)"
end
end
...
...
app/models/concerns/milestoneish.rb
View file @
49b85262
...
...
@@ -102,14 +102,14 @@ module Milestoneish
Gitlab
::
TimeTrackingFormatter
.
output
(
total_issue_time_estimate
)
end
private
def
count_issues_by_state
(
user
)
memoize_per_user
(
user
,
:count_issues_by_state
)
do
issues_visible_to_user
(
user
).
reorder
(
nil
).
group
(
:state
).
count
end
end
private
def
memoize_per_user
(
user
,
method_name
)
memoized_users
[
method_name
][
user
&
.
id
]
||=
yield
end
...
...
app/serializers/entity_date_helper.rb
View file @
49b85262
module
EntityDateHelper
include
ActionView
::
Helpers
::
DateHelper
include
ActionView
::
Helpers
::
TagHelper
def
interval_in_words
(
diff
)
return
'Not started'
unless
diff
...
...
@@ -34,4 +35,30 @@ module EntityDateHelper
duration_hash
end
# Generates an HTML-formatted string for remaining dates based on start_date and due_date
#
# It returns "Past due" for expired entities
# It returns "Upcoming" for upcoming entities
# If due date is provided, it returns "# days|weeks|months remaining|ago"
# If start date is provided and elapsed, with no due date, it returns "# days elapsed"
def
remaining_days_in_words
(
entity
)
if
entity
.
try
(
:expired?
)
content_tag
(
:strong
,
'Past due'
)
elsif
entity
.
try
(
:upcoming?
)
content_tag
(
:strong
,
'Upcoming'
)
elsif
entity
.
due_date
is_upcoming
=
(
entity
.
due_date
-
Date
.
today
).
to_i
>
0
time_ago
=
time_ago_in_words
(
entity
.
due_date
)
content
=
time_ago
.
gsub
(
/\d+/
)
{
|
match
|
"<strong>
#{
match
}
</strong>"
}
content
.
slice!
(
"about "
)
content
<<
" "
+
(
is_upcoming
?
_
(
"remaining"
)
:
_
(
"ago"
))
content
.
html_safe
elsif
entity
.
start_date
&&
entity
.
start_date
.
past?
days
=
entity
.
elapsed_days
content
=
content_tag
(
:strong
,
days
)
content
<<
"
#{
'day'
.
pluralize
(
days
)
}
elapsed"
content
.
html_safe
end
end
end
app/views/projects/issues/_issue.html.haml
View file @
49b85262
...
...
@@ -26,7 +26,7 @@
-
if
issue
.
milestone
%span
.issuable-milestone.hidden-xs
=
link_to
project_issues_path
(
issue
.
project
,
milestone_title:
issue
.
milestone
.
title
),
data:
{
html:
1
,
toggle:
'tooltip'
,
title:
issuable_milestone_tooltip_title
(
issu
e
)
}
do
=
link_to
project_issues_path
(
issue
.
project
,
milestone_title:
issue
.
milestone
.
title
),
data:
{
html:
1
,
toggle:
'tooltip'
,
title:
milestone_tooltip_due_date
(
issue
.
mileston
e
)
}
do
=
icon
(
'clock-o'
)
=
issue
.
milestone
.
title
-
if
issue
.
due_date
...
...
app/views/projects/merge_requests/_merge_request.html.haml
View file @
49b85262
...
...
@@ -23,7 +23,7 @@
-
if
merge_request
.
milestone
%span
.issuable-milestone.hidden-xs
=
link_to
project_merge_requests_path
(
merge_request
.
project
,
milestone_title:
merge_request
.
milestone
.
title
),
data:
{
html:
1
,
toggle:
'tooltip'
,
title:
issuable_milestone_tooltip_title
(
merge_request
)
}
do
=
link_to
project_merge_requests_path
(
merge_request
.
project
,
milestone_title:
merge_request
.
milestone
.
title
),
data:
{
html:
1
,
toggle:
'tooltip'
,
title:
milestone_tooltip_due_date
(
merge_request
.
milestone
)
}
do
=
icon
(
'clock-o'
)
=
merge_request
.
milestone
.
title
-
if
merge_request
.
target_project
.
default_branch
!=
merge_request
.
target_branch
...
...
app/views/shared/issuable/_sidebar.html.haml
View file @
49b85262
...
...
@@ -7,7 +7,7 @@
-
if
current_user
%span
.issuable-header-text.hide-collapsed.pull-left
=
_
(
'Todo'
)
%a
.gutter-toggle.pull-right.js-sidebar-toggle
{
role:
"button"
,
href:
"#"
,
"aria-label"
=>
"Toggle sidebar"
}
%a
.gutter-toggle.pull-right.js-sidebar-toggle
.has-tooltip
{
role:
"button"
,
href:
"#"
,
"aria-label"
=>
"Toggle sidebar"
,
title:
sidebar_gutter_tooltip_text
,
data:
{
container:
'body'
,
placement:
'left'
}
}
=
sidebar_gutter_toggle_icon
-
if
current_user
=
render
"shared/issuable/sidebar_todo"
,
todo:
todo
,
issuable:
issuable
...
...
@@ -19,12 +19,11 @@
.block.assignee
=
render
"shared/issuable/sidebar_assignees"
,
issuable:
issuable
,
can_edit_issuable:
can_edit_issuable
,
signed_in:
current_user
.
present?
.block.milestone
.sidebar-collapsed-icon
.sidebar-collapsed-icon
.has-tooltip
{
title:
milestone_tooltip_title
(
issuable
.
milestone
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
=
icon
(
'clock-o'
,
'aria-hidden'
:
'true'
)
%span
.milestone-title
-
if
issuable
.
milestone
%span
.has-tooltip
{
title:
"#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}"
,
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
=
issuable
.
milestone
.
title
=
issuable
.
milestone
.
title
-
else
=
_
(
'None'
)
.title.hide-collapsed
...
...
@@ -34,7 +33,7 @@
=
link_to
_
(
'Edit'
),
'#'
,
class:
'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
-
if
issuable
.
milestone
=
link_to
issuable
.
milestone
.
title
,
milestone_path
(
issuable
.
milestone
),
class:
"bold has-tooltip"
,
title:
milestone_tooltip_
titl
e
(
issuable
.
milestone
),
data:
{
container:
"body"
,
html:
1
}
=
link_to
issuable
.
milestone
.
title
,
milestone_path
(
issuable
.
milestone
),
class:
"bold has-tooltip"
,
title:
milestone_tooltip_
due_dat
e
(
issuable
.
milestone
),
data:
{
container:
"body"
,
html:
1
}
-
else
%span
.no-value
=
_
(
'None'
)
...
...
@@ -50,7 +49,7 @@
=
icon
(
'spinner spin'
,
'aria-hidden'
:
'true'
)
-
if
issuable
.
has_attribute?
(
:due_date
)
.block.due_date
.sidebar-collapsed-icon
.sidebar-collapsed-icon
.has-tooltip
{
data:
{
placement:
'left'
,
container:
'body'
,
html:
1
},
title:
sidebar_due_date_tooltip_label
(
issuable
)
}
=
icon
(
'calendar'
,
'aria-hidden'
:
'true'
)
%span
.js-due-date-sidebar-value
=
issuable
.
due_date
.
try
(
:to_s
,
:medium
)
||
'None'
...
...
app/views/shared/issuable/_sidebar_assignees.html.haml
View file @
49b85262
...
...
@@ -4,7 +4,7 @@
=
_
(
'Assignee'
)
=
icon
(
'spinner spin'
)
-
else
.sidebar-collapsed-icon.sidebar-collapsed-user
{
data:
{
toggle:
"tooltip"
,
placement:
"left"
,
container:
"body"
},
title:
(
issuable
.
assignee
.
name
if
issuable
.
assigne
e
)
}
.sidebar-collapsed-icon.sidebar-collapsed-user
{
data:
{
toggle:
"tooltip"
,
placement:
"left"
,
container:
"body"
},
title:
sidebar_assignee_tooltip_label
(
issuabl
e
)
}
-
if
issuable
.
assignee
=
link_to_member
(
@project
,
issuable
.
assignee
,
size:
24
)
-
else
...
...
app/views/shared/issuable/_sidebar_todo.html.haml
View file @
49b85262
-
is_collapsed
=
local_assigns
.
fetch
(
:is_collapsed
,
false
)
-
mark_content
=
is_collapsed
?
icon
(
'check-square'
,
class:
'todo-undone'
)
:
_
(
'Mark done'
)
-
mark_content
=
is_collapsed
?
icon
(
'check-square'
,
class:
'todo-undone'
)
:
_
(
'Mark
todo as
done'
)
-
todo_content
=
is_collapsed
?
icon
(
'plus-square'
)
:
_
(
'Add todo'
)
%button
.issuable-todo-btn.js-issuable-todo
{
type:
'button'
,
class:
(
is_collapsed
?
'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip'
:
'btn btn-default issuable-header-btn pull-right'
),
title:
(
todo
.
nil?
?
_
(
'Add todo'
)
:
_
(
'Mark done'
)),
'aria-label'
=>
(
todo
.
nil?
?
_
(
'Add todo'
)
:
_
(
'Mark done'
)),
title:
(
todo
.
nil?
?
_
(
'Add todo'
)
:
_
(
'Mark
todo as
done'
)),
'aria-label'
=>
(
todo
.
nil?
?
_
(
'Add todo'
)
:
_
(
'Mark
todo as
done'
)),
data:
issuable_todo_button_data
(
issuable
,
todo
,
is_collapsed
)
}
%span
.issuable-todo-inner.js-issuable-todo-inner
<
-
if
todo
...
...
app/views/shared/issuable/form/_merge_request_assignee.html.haml
View file @
49b85262
-
merge_request
=
issuable
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user
{
data:
{
toggle:
"tooltip"
,
placement:
"left"
,
container:
"body"
},
title:
(
merge_request
.
assignee
.
name
if
merge_request
.
assigne
e
)
}
.sidebar-collapsed-icon.sidebar-collapsed-user
{
data:
{
toggle:
"tooltip"
,
placement:
"left"
,
container:
"body"
},
title:
sidebar_assignee_tooltip_label
(
issuabl
e
)
}
-
if
merge_request
.
assignee
=
link_to_member
(
@project
,
merge_request
.
assignee
,
size:
24
)
-
else
...
...
app/views/shared/milestones/_sidebar.html.haml
View file @
49b85262
...
...
@@ -4,12 +4,8 @@
%aside
.right-sidebar.js-right-sidebar
{
data:
{
"offset-top"
=>
affix_offset
,
"spy"
=>
"affix"
,
"always-show-toggle"
=>
true
},
class:
sidebar_gutter_collapsed_class
,
'aria-live'
=>
'polite'
}
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
%a
.gutter-toggle.pull-right.js-sidebar-toggle
{
role:
"button"
,
href:
"#"
,
"aria-label"
=>
"Toggle sidebar"
}
%a
.gutter-toggle.pull-right.js-sidebar-toggle
.has-tooltip
{
role:
"button"
,
href:
"#"
,
"aria-label"
=>
"Toggle sidebar"
,
title:
sidebar_gutter_tooltip_text
,
data:
{
container:
'body'
,
placement:
'left'
}
}
=
sidebar_gutter_toggle_icon
.sidebar-collapsed-icon
%span
==
#{
milestone
.
percent_complete
(
current_user
)
}
%
=
milestone_progress_bar
(
milestone
)
.title.hide-collapsed
%strong
.bold
==
#{
milestone
.
percent_complete
(
current_user
)
}
%
%span
.hide-collapsed
...
...
@@ -17,6 +13,11 @@
.value.hide-collapsed
=
milestone_progress_bar
(
milestone
)
.block.milestone-progress.hide-expanded
.sidebar-collapsed-icon.has-tooltip
{
title:
milestone_progress_tooltip_text
(
milestone
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
%span
==
#{
milestone
.
percent_complete
(
current_user
)
}
%
=
milestone_progress_bar
(
milestone
)
.block.start_date.hide-collapsed
.title
Start date
...
...
@@ -35,19 +36,25 @@
%span
.collapsed-milestone-date
-
if
milestone
.
start_date
&&
milestone
.
due_date
-
if
milestone
.
start_date
.
year
==
milestone
.
due_date
.
year
.milestone-date
=
milestone
.
start_date
.
strftime
(
'%b %-d'
)
.milestone-date.has-tooltip
{
title:
milestone_time_for
(
milestone
.
start_date
,
:start
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
=
milestone
.
start_date
.
strftime
(
'%b %-d'
)
-
else
.milestone-date
=
milestone
.
start_date
.
strftime
(
'%b %-d %Y'
)
.milestone-date.has-tooltip
{
title:
milestone_time_for
(
milestone
.
start_date
,
:start
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
=
milestone
.
start_date
.
strftime
(
'%b %-d %Y'
)
.date-separator
-
.due_date
=
milestone
.
due_date
.
strftime
(
'%b %-d %Y'
)
.due_date.has-tooltip
{
title:
milestone_time_for
(
milestone
.
due_date
,
:end
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
=
milestone
.
due_date
.
strftime
(
'%b %-d %Y'
)
-
elsif
milestone
.
start_date
From
.milestone-date
=
milestone
.
start_date
.
strftime
(
'%b %-d %Y'
)
.milestone-date.has-tooltip
{
title:
milestone_time_for
(
milestone
.
start_date
,
:start
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
=
milestone
.
start_date
.
strftime
(
'%b %-d %Y'
)
-
elsif
milestone
.
due_date
Until
.milestone-date
=
milestone
.
due_date
.
strftime
(
'%b %-d %Y'
)
.milestone-date.has-tooltip
{
title:
milestone_time_for
(
milestone
.
due_date
,
:end
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
=
milestone
.
due_date
.
strftime
(
'%b %-d %Y'
)
-
else
None
.has-tooltip
{
title:
milestone_time_for
(
milestone
.
start_date
,
:start
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
None
.title.hide-collapsed
Due date
-
if
@project
&&
can?
(
current_user
,
:admin_milestone
,
@project
)
...
...
@@ -58,14 +65,14 @@
%span
.bold
=
milestone
.
due_date
.
to_s
(
:medium
)
-
else
%span
.no-value
No due date
-
remaining_days
=
milestone_remaining_day
s
(
milestone
)
-
remaining_days
=
remaining_days_in_word
s
(
milestone
)
-
if
remaining_days
.
present?
=
surround
'('
,
')'
do
%span
.remaining-days
=
remaining_days
-
if
!
project
||
can?
(
current_user
,
:read_issue
,
project
)
.block.issues
.sidebar-collapsed-icon
.sidebar-collapsed-icon
.has-tooltip
{
title:
milestone_issues_tooltip_text
(
milestone
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
%strong
=
custom_icon
(
'issues'
)
%span
=
milestone
.
issues_visible_to_user
(
current_user
).
count
...
...
@@ -93,7 +100,7 @@
=
icon
(
'spinner spin'
)
.block.merge-requests
.sidebar-collapsed-icon
.sidebar-collapsed-icon
.has-tooltip
{
title:
milestone_merge_requests_tooltip_text
(
milestone
),
data:
{
container:
'body'
,
html:
1
,
placement:
'left'
}
}
%strong
=
custom_icon
(
'mr_bold'
)
%span
=
milestone
.
merge_requests
.
count
...
...
changelogs/unreleased/25010-collapsed-sidebar-tooltips.yml
0 → 100644
View file @
49b85262
---
title
:
Improve tooltips in collapsed right sidebar
merge_request
:
17714
author
:
type
:
changed
doc/user/project/issues/issues_functionalities.md
View file @
49b85262
...
...
@@ -28,7 +28,7 @@ Comments and system notes also appear automatically in response to various actio
#### 2. Todos
-
Add todo: add that issue to your
[
GitLab Todo
](
../../../workflow/todos.html
)
list
-
Mark done: mark that issue as done (reflects on the Todo list)
-
Mark
todo as
done: mark that issue as done (reflects on the Todo list)
#### 3. Assignee
...
...
doc/workflow/todos.md
View file @
49b85262
...
...
@@ -92,9 +92,9 @@ corresponding **Done** button, and it will disappear from your Todo list.
![
A Todo in the Todos dashboard
](
img/todo_list_item.png
)
A Todo can also be marked as done from the issue or merge request sidebar using
the "Mark done" button.
the "Mark
todo as
done" button.
![
Mark
D
one from the issuable sidebar
](
img/todos_mark_done_sidebar.png
)
![
Mark
todo as d
one from the issuable sidebar
](
img/todos_mark_done_sidebar.png
)
You can mark all your Todos as done at once by clicking on the
**
Mark all as
done
**
button.
...
...
spec/features/issues/todo_spec.rb
View file @
49b85262
...
...
@@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do
it
'creates todo when clicking button'
do
page
.
within
'.issuable-sidebar'
do
click_button
'Add todo'
expect
(
page
).
to
have_content
'Mark done'
expect
(
page
).
to
have_content
'Mark
todo as
done'
end
page
.
within
'.header-content .todos-count'
do
...
...
@@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do
it
'marks a todo as done'
do
page
.
within
'.issuable-sidebar'
do
click_button
'Add todo'
click_button
'Mark done'
click_button
'Mark
todo as
done'
end
expect
(
page
).
to
have_selector
(
'.todos-count'
,
visible:
false
)
...
...
spec/helpers/issuables_helper_spec.rb
View file @
49b85262
...
...
@@ -22,11 +22,15 @@ describe IssuablesHelper do
end
describe
'#issuable_labels_tooltip'
do
it
'returns label text'
do
it
'returns label text with no labels'
do
expect
(
issuable_labels_tooltip
([])).
to
eq
(
"Labels"
)
end
it
'returns label text with labels within max limit'
do
expect
(
issuable_labels_tooltip
([
label
])).
to
eq
(
label
.
title
)
end
it
'returns label text'
do
it
'returns label text
with labels exceeding max limit
'
do
expect
(
issuable_labels_tooltip
([
label
,
label2
],
limit:
1
)).
to
eq
(
"
#{
label
.
title
}
, and 1 more"
)
end
end
...
...
spec/helpers/milestones_helper_spec.rb
View file @
49b85262
...
...
@@ -83,58 +83,4 @@ describe MilestonesHelper do
end
end
end
describe
'#milestone_remaining_days'
do
around
do
|
example
|
Timecop
.
freeze
(
Time
.
utc
(
2017
,
3
,
17
))
{
example
.
run
}
end
context
'when less than 31 days remaining'
do
let
(
:milestone_remaining
)
{
milestone_remaining_days
(
build_stubbed
(
:milestone
,
due_date:
12
.
days
.
from_now
.
utc
))
}
it
'returns days remaining'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>12</strong> days remaining"
)
end
end
context
'when less than 1 year and more than 30 days remaining'
do
let
(
:milestone_remaining
)
{
milestone_remaining_days
(
build_stubbed
(
:milestone
,
due_date:
2
.
months
.
from_now
.
utc
))
}
it
'returns months remaining'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>2</strong> months remaining"
)
end
end
context
'when more than 1 year remaining'
do
let
(
:milestone_remaining
)
{
milestone_remaining_days
(
build_stubbed
(
:milestone
,
due_date:
(
1
.
year
.
from_now
+
2
.
days
).
utc
))
}
it
'returns years remaining'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>1</strong> year remaining"
)
end
end
context
'when milestone is expired'
do
let
(
:milestone_remaining
)
{
milestone_remaining_days
(
build_stubbed
(
:milestone
,
due_date:
2
.
days
.
ago
.
utc
))
}
it
'returns "Past due"'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>Past due</strong>"
)
end
end
context
'when milestone has start_date in the future'
do
let
(
:milestone_remaining
)
{
milestone_remaining_days
(
build_stubbed
(
:milestone
,
start_date:
2
.
days
.
from_now
.
utc
))
}
it
'returns "Upcoming"'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>Upcoming</strong>"
)
end
end
context
'when milestone has start_date in the past'
do
let
(
:milestone_remaining
)
{
milestone_remaining_days
(
build_stubbed
(
:milestone
,
start_date:
2
.
days
.
ago
.
utc
))
}
it
'returns days elapsed'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>2</strong> days elapsed"
)
end
end
end
end
spec/javascripts/collapsed_sidebar_todo_spec.js
View file @
49b85262
...
...
@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout
(()
=>
{
expect
(
document
.
querySelector
(
'.issuable-sidebar-header .js-issuable-todo'
).
textContent
.
trim
(),
).
toBe
(
'Mark done'
);
).
toBe
(
'Mark
todo as
done'
);
done
();
});
...
...
@@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout
(()
=>
{
expect
(
document
.
querySelector
(
'.js-issuable-todo.sidebar-collapsed-icon'
).
getAttribute
(
'data-original-title'
),
).
toBe
(
'Mark done'
);
).
toBe
(
'Mark
todo as
done'
);
done
();
});
...
...
@@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.
catch
(
done
.
fail
);
});
it
(
'updates aria-label to mark done'
,
(
done
)
=>
{
it
(
'updates aria-label to mark
todo as
done'
,
(
done
)
=>
{
document
.
querySelector
(
'.js-issuable-todo.sidebar-collapsed-icon'
).
click
();
setTimeout
(()
=>
{
expect
(
document
.
querySelector
(
'.js-issuable-todo.sidebar-collapsed-icon'
).
getAttribute
(
'aria-label'
),
).
toBe
(
'Mark done'
);
).
toBe
(
'Mark
todo as
done'
);
done
();
});
...
...
@@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.
then
(()
=>
{
expect
(
document
.
querySelector
(
'.js-issuable-todo.sidebar-collapsed-icon'
).
getAttribute
(
'aria-label'
),
).
toBe
(
'Mark done'
);
).
toBe
(
'Mark
todo as
done'
);
document
.
querySelector
(
'.js-issuable-todo.sidebar-collapsed-icon'
).
click
();
})
...
...
spec/models/milestone_spec.rb
View file @
49b85262
...
...
@@ -96,7 +96,9 @@ describe Milestone do
allow
(
milestone
).
to
receive
(
:due_date
).
and_return
(
Date
.
today
.
prev_year
)
end
it
{
expect
(
milestone
.
expired?
).
to
be_truthy
}
it
'returns true when due_date is in the past'
do
expect
(
milestone
.
expired?
).
to
be_truthy
end
end
context
"not expired"
do
...
...
@@ -104,17 +106,19 @@ describe Milestone do
allow
(
milestone
).
to
receive
(
:due_date
).
and_return
(
Date
.
today
.
next_year
)
end
it
{
expect
(
milestone
.
expired?
).
to
be_falsey
}
it
'returns false when due_date is in the future'
do
expect
(
milestone
.
expired?
).
to
be_falsey
end
end
end
describe
'#upcoming?'
do
it
'returns true'
do
it
'returns true
when start_date is in the future
'
do
milestone
=
build
(
:milestone
,
start_date:
Time
.
now
+
1
.
month
)
expect
(
milestone
.
upcoming?
).
to
be_truthy
end
it
'returns false'
do
it
'returns false
when start_date is in the past
'
do
milestone
=
build
(
:milestone
,
start_date:
Date
.
today
.
prev_year
)
expect
(
milestone
.
upcoming?
).
to
be_falsey
end
...
...
spec/serializers/entity_date_helper_spec.rb
View file @
49b85262
...
...
@@ -32,6 +32,7 @@ describe EntityDateHelper do
end
it
'converts 86560 seconds'
do
Rails
.
logger
.
debug
date_helper_class
.
inspect
expect
(
date_helper_class
.
distance_of_time_as_hash
(
86560
)).
to
eq
(
days:
1
,
mins:
2
,
seconds:
40
)
end
...
...
@@ -42,4 +43,58 @@ describe EntityDateHelper do
it
'converts 986760 seconds'
do
expect
(
date_helper_class
.
distance_of_time_as_hash
(
986760
)).
to
eq
(
days:
11
,
hours:
10
,
mins:
6
)
end
describe
'#remaining_days_in_words'
do
around
do
|
example
|
Timecop
.
freeze
(
Time
.
utc
(
2017
,
3
,
17
))
{
example
.
run
}
end
context
'when less than 31 days remaining'
do
let
(
:milestone_remaining
)
{
date_helper_class
.
remaining_days_in_words
(
build_stubbed
(
:milestone
,
due_date:
12
.
days
.
from_now
.
utc
))
}
it
'returns days remaining'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>12</strong> days remaining"
)
end
end
context
'when less than 1 year and more than 30 days remaining'
do
let
(
:milestone_remaining
)
{
date_helper_class
.
remaining_days_in_words
(
build_stubbed
(
:milestone
,
due_date:
2
.
months
.
from_now
.
utc
))
}
it
'returns months remaining'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>2</strong> months remaining"
)
end
end
context
'when more than 1 year remaining'
do
let
(
:milestone_remaining
)
{
date_helper_class
.
remaining_days_in_words
(
build_stubbed
(
:milestone
,
due_date:
(
1
.
year
.
from_now
+
2
.
days
).
utc
))
}
it
'returns years remaining'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>1</strong> year remaining"
)
end
end
context
'when milestone is expired'
do
let
(
:milestone_remaining
)
{
date_helper_class
.
remaining_days_in_words
(
build_stubbed
(
:milestone
,
due_date:
2
.
days
.
ago
.
utc
))
}
it
'returns "Past due"'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>Past due</strong>"
)
end
end
context
'when milestone has start_date in the future'
do
let
(
:milestone_remaining
)
{
date_helper_class
.
remaining_days_in_words
(
build_stubbed
(
:milestone
,
start_date:
2
.
days
.
from_now
.
utc
))
}
it
'returns "Upcoming"'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>Upcoming</strong>"
)
end
end
context
'when milestone has start_date in the past'
do
let
(
:milestone_remaining
)
{
date_helper_class
.
remaining_days_in_words
(
build_stubbed
(
:milestone
,
start_date:
2
.
days
.
ago
.
utc
))
}
it
'returns days elapsed'
do
expect
(
milestone_remaining
).
to
eq
(
"<strong>2</strong> days elapsed"
)
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment