Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
9652f792
Commit
9652f792
authored
Dec 08, 2018
by
Felipe Artur
Committed by
Phil Hughes
Dec 08, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Epic issue list and related issue list re-design
parent
245e1483
Changes
35
Hide whitespace changes
Inline
Side-by-side
Showing
35 changed files
with
1414 additions
and
94 deletions
+1414
-94
app/assets/javascripts/boards/components/issue_due_date.vue
app/assets/javascripts/boards/components/issue_due_date.vue
+14
-6
app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
...vascripts/vue_shared/components/issue/issue_assignees.vue
+94
-0
app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
...vascripts/vue_shared/components/issue/issue_milestone.vue
+90
-0
doc/user/group/epics/img/containing_epic.png
doc/user/group/epics/img/containing_epic.png
+0
-0
doc/user/group/epics/img/epic_view.png
doc/user/group/epics/img/epic_view.png
+0
-0
ee/app/assets/javascripts/boards/components/issue_card_weight.vue
...ssets/javascripts/boards/components/issue_card_weight.vue
+12
-11
ee/app/assets/javascripts/related_issues/components/add_issuable_form.vue
...vascripts/related_issues/components/add_issuable_form.vue
+5
-0
ee/app/assets/javascripts/related_issues/components/issue_item.vue
...sets/javascripts/related_issues/components/issue_item.vue
+79
-11
ee/app/assets/javascripts/related_issues/components/related_issues_block.vue
...cripts/related_issues/components/related_issues_block.vue
+21
-11
ee/app/assets/javascripts/related_issues/components/related_issues_root.vue
...scripts/related_issues/components/related_issues_root.vue
+1
-0
ee/app/assets/javascripts/related_issues/mixins/related_issues_mixin.js
...javascripts/related_issues/mixins/related_issues_mixin.js
+61
-1
ee/app/assets/stylesheets/components/related_items_list.scss
ee/app/assets/stylesheets/components/related_items_list.scss
+361
-0
ee/app/assets/stylesheets/pages/issues/related_issues.scss
ee/app/assets/stylesheets/pages/issues/related_issues.scss
+0
-1
ee/app/models/ee/epic.rb
ee/app/models/ee/epic.rb
+2
-1
ee/app/services/epic_issues/list_service.rb
ee/app/services/epic_issues/list_service.rb
+1
-1
ee/app/services/issuable_links/list_service.rb
ee/app/services/issuable_links/list_service.rb
+14
-1
ee/app/services/issue_links/list_service.rb
ee/app/services/issue_links/list_service.rb
+1
-1
ee/changelogs/unreleased/issue_6086.yml
ee/changelogs/unreleased/issue_6086.yml
+5
-0
ee/spec/controllers/groups/epic_issues_controller_spec.rb
ee/spec/controllers/groups/epic_issues_controller_spec.rb
+4
-14
ee/spec/features/epics/epic_issues_spec.rb
ee/spec/features/epics/epic_issues_spec.rb
+5
-5
ee/spec/features/issuables/related_issues_spec.rb
ee/spec/features/issuables/related_issues_spec.rb
+9
-9
ee/spec/fixtures/api/schemas/related_issue.json
ee/spec/fixtures/api/schemas/related_issue.json
+33
-0
ee/spec/fixtures/api/schemas/related_issues.json
ee/spec/fixtures/api/schemas/related_issues.json
+4
-0
ee/spec/javascripts/boards/issue_card_spec.js
ee/spec/javascripts/boards/issue_card_spec.js
+1
-1
ee/spec/javascripts/epic/components/epic_body_spec.js
ee/spec/javascripts/epic/components/epic_body_spec.js
+1
-1
ee/spec/javascripts/issuable/related_issues/components/add_issuable_form_spec.js
...uable/related_issues/components/add_issuable_form_spec.js
+9
-0
ee/spec/javascripts/issuable/related_issues/components/issue_item_spec.js
...pts/issuable/related_issues/components/issue_item_spec.js
+76
-11
ee/spec/javascripts/issuable/related_issues/components/issue_token_spec.js
...ts/issuable/related_issues/components/issue_token_spec.js
+11
-0
ee/spec/javascripts/issuable/related_issues/mock_data.js
ee/spec/javascripts/issuable/related_issues/mock_data.js
+56
-0
ee/spec/services/epic_issues/list_service_spec.rb
ee/spec/services/epic_issues/list_service_spec.rb
+72
-6
ee/spec/services/issue_links/list_service_spec.rb
ee/spec/services/issue_links/list_service_spec.rb
+3
-2
locale/gitlab.pot
locale/gitlab.pot
+12
-0
spec/javascripts/boards/mock_data.js
spec/javascripts/boards/mock_data.js
+9
-0
spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
...ripts/vue_shared/components/issue/issue_assignees_spec.js
+114
-0
spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
...ripts/vue_shared/components/issue/issue_milestone_spec.js
+234
-0
No files found.
app/assets/javascripts/boards/components/issue_due_date.vue
View file @
9652f792
...
...
@@ -15,6 +15,16 @@ export default {
type
:
String
,
required
:
true
,
},
cssClass
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
tooltipPlacement
:
{
type
:
String
,
required
:
false
,
default
:
'
bottom
'
,
},
},
computed
:
{
title
()
{
...
...
@@ -66,15 +76,13 @@ export default {
<
template
>
<span>
<span
ref=
"issueDueDate"
class=
"board-card-info card-number"
>
<icon
:class=
"
{ 'text-danger': isPastDue, 'board-card-info-icon': true }"
name="calendar"
/>
<time
:class=
"
{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">
{{
<span
ref=
"issueDueDate"
:class=
"cssClass"
class=
"board-card-info card-number"
>
<icon
:class=
"
{ 'text-danger': isPastDue, 'board-card-info-icon': true }" name="calendar" />
<time
:class=
"
{ 'text-danger': isPastDue }" datetime="date" class="board-card-info-text">
{{
body
}}
</time>
</span>
<gl-tooltip
:target=
"() => $refs.issueDueDate"
placement=
"bottom
"
>
<gl-tooltip
:target=
"() => $refs.issueDueDate"
:placement=
"tooltipPlacement
"
>
<span
class=
"bold"
>
{{
__
(
'
Due date
'
)
}}
</span>
<br
/>
<span
:class=
"
{ 'text-danger-muted': isPastDue }">
{{
title
}}
</span>
</gl-tooltip>
...
...
app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue
0 → 100644
View file @
9652f792
<
script
>
import
{
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
UserAvatarLink
from
'
~/vue_shared/components/user_avatar/user_avatar_link.vue
'
;
export
default
{
components
:
{
UserAvatarLink
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
assignees
:
{
type
:
Array
,
required
:
true
,
},
},
data
()
{
return
{
maxVisibleAssignees
:
2
,
maxAssigneeAvatars
:
3
,
maxAssignees
:
99
,
};
},
computed
:
{
countOverLimit
()
{
return
this
.
assignees
.
length
-
this
.
maxVisibleAssignees
;
},
assigneesToShow
()
{
if
(
this
.
assignees
.
length
>
this
.
maxAssigneeAvatars
)
{
return
this
.
assignees
.
slice
(
0
,
this
.
maxVisibleAssignees
);
}
return
this
.
assignees
;
},
assigneesCounterTooltip
()
{
const
{
countOverLimit
,
maxAssignees
}
=
this
;
const
count
=
countOverLimit
>
maxAssignees
?
maxAssignees
:
countOverLimit
;
return
sprintf
(
__
(
'
%{count} more assignees
'
),
{
count
});
},
shouldRenderAssigneesCounter
()
{
const
assigneesCount
=
this
.
assignees
.
length
;
if
(
assigneesCount
<=
this
.
maxAssigneeAvatars
)
{
return
false
;
}
return
assigneesCount
>
this
.
countOverLimit
;
},
assigneeCounterLabel
()
{
if
(
this
.
countOverLimit
>
this
.
maxAssignees
)
{
return
`
${
this
.
maxAssignees
}
+`
;
}
return
`+
${
this
.
countOverLimit
}
`
;
},
},
methods
:
{
avatarUrlTitle
(
assignee
)
{
return
sprintf
(
__
(
'
Avatar for %{assigneeName}
'
),
{
assigneeName
:
assignee
.
name
,
});
},
},
};
</
script
>
<
template
>
<div
class=
"issue-assignees"
>
<user-avatar-link
v-for=
"assignee in assigneesToShow"
:key=
"assignee.id"
:link-href=
"assignee.web_url"
:img-alt=
"avatarUrlTitle(assignee)"
:img-src=
"assignee.avatar_url"
:img-size=
"24"
class=
"js-no-trigger"
tooltip-placement=
"bottom"
>
<span
class=
"js-assignee-tooltip"
>
<span
class=
"bold d-block"
>
{{
__
(
'
Assignee
'
)
}}
</span>
{{
assignee
.
name
}}
<span
class=
"text-white-50"
>
@
{{
assignee
.
username
}}
</span>
</span>
</user-avatar-link>
<span
v-if=
"shouldRenderAssigneesCounter"
v-gl-tooltip
:title=
"assigneesCounterTooltip"
class=
"avatar-counter"
data-placement=
"bottom"
>
{{
assigneeCounterLabel
}}
</span
>
</div>
</
template
>
app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue
0 → 100644
View file @
9652f792
<
script
>
import
{
GlTooltip
}
from
'
@gitlab/ui
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
timeagoMixin
from
'
~/vue_shared/mixins/timeago
'
;
import
{
timeFor
,
parsePikadayDate
,
dateInWords
}
from
'
~/lib/utils/datetime_utility
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
components
:
{
Icon
,
GlTooltip
,
},
mixins
:
[
timeagoMixin
],
props
:
{
milestone
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
milestoneDue
:
this
.
milestone
.
due_date
?
parsePikadayDate
(
this
.
milestone
.
due_date
)
:
null
,
milestoneStart
:
this
.
milestone
.
start_date
?
parsePikadayDate
(
this
.
milestone
.
start_date
)
:
null
,
};
},
computed
:
{
isMilestoneStarted
()
{
if
(
!
this
.
milestoneStart
)
{
return
false
;
}
return
Date
.
now
()
>
this
.
milestoneStart
;
},
isMilestonePastDue
()
{
if
(
!
this
.
milestoneDue
)
{
return
false
;
}
return
Date
.
now
()
>
this
.
milestoneDue
;
},
milestoneDatesAbsolute
()
{
if
(
this
.
milestoneDue
)
{
return
`(
${
dateInWords
(
this
.
milestoneDue
)}
)`
;
}
else
if
(
this
.
milestoneStart
)
{
return
`(
${
dateInWords
(
this
.
milestoneStart
)}
)`
;
}
return
''
;
},
milestoneDatesHuman
()
{
if
(
this
.
milestoneStart
||
this
.
milestoneDue
)
{
if
(
this
.
milestoneDue
)
{
return
timeFor
(
this
.
milestoneDue
,
sprintf
(
__
(
'
Expired %{expiredOn}
'
),
{
expiredOn
:
this
.
timeFormated
(
this
.
milestoneDue
),
}),
);
}
return
sprintf
(
this
.
isMilestoneStarted
?
__
(
'
Started %{startsIn}
'
)
:
__
(
'
Starts %{startsIn}
'
),
{
startsIn
:
this
.
timeFormated
(
this
.
milestoneStart
),
},
);
}
return
''
;
},
},
};
</
script
>
<
template
>
<div
ref=
"milestoneDetails"
class=
"issue-milestone-details"
>
<icon
:size=
"16"
class=
"inline icon"
name=
"clock"
/>
<span
class=
"milestone-title"
>
{{
milestone
.
title
}}
</span>
<gl-tooltip
:target=
"() => $refs.milestoneDetails"
placement=
"bottom"
class=
"js-item-milestone"
>
<span
class=
"bold"
>
{{
__
(
'
Milestone
'
)
}}
</span>
<br
/>
<span>
{{
milestone
.
title
}}
</span>
<br
/>
<span
v-if=
"milestoneStart || milestoneDue"
:class=
"
{
'text-danger-muted': isMilestonePastDue,
'text-tertiary': !isMilestonePastDue,
}"
>
<span>
{{
milestoneDatesHuman
}}
</span
><br
/><span>
{{
milestoneDatesAbsolute
}}
</span>
</span>
</gl-tooltip>
</div>
</
template
>
doc/user/group/epics/img/containing_epic.png
View replaced file @
245e1483
View file @
9652f792
75.8 KB
|
W:
|
H:
313 KB
|
W:
|
H:
2-up
Swipe
Onion skin
doc/user/group/epics/img/epic_view.png
View replaced file @
245e1483
View file @
9652f792
130 KB
|
W:
|
H:
336 KB
|
W:
|
H:
2-up
Swipe
Onion skin
ee/app/assets/javascripts/boards/components/issue_card_weight.vue
View file @
9652f792
<
script
>
import
{
GlTooltip
Directive
}
from
'
@gitlab/ui
'
;
import
{
GlTooltip
}
from
'
@gitlab/ui
'
;
import
icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
name
:
'
IssueCardWeight
'
,
components
:
{
icon
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
GlTooltip
,
},
props
:
{
weight
:
{
...
...
@@ -21,16 +19,19 @@ export default {
<
template
>
<a
v-gl-tooltip
:title=
"__('Weight')"
ref=
"itemWeight"
class=
"board-card-info card-number board-card-weight"
data-container=
"body"
data-placement=
"bottom"
tabindex=
"1"
v-on=
"$listeners"
>
<icon
name=
"weight"
css-classes=
"board-card-info-icon"
/><span
class=
"board-card-info-text"
>
{{
weight
}}
</span>
<icon
name=
"weight"
css-classes=
"board-card-info-icon"
/>
<span
class=
"board-card-info-text"
>
{{
weight
}}
</span>
<gl-tooltip
:target=
"() => $refs.itemWeight"
placement=
"bottom"
container=
"body"
class=
"js-item-weight"
>
{{
__
(
'
Weight
'
)
}}
<br
/><span
class=
"text-tertiary"
>
{{
weight
}}
</span>
</gl-tooltip>
</a>
</
template
>
ee/app/assets/javascripts/related_issues/components/add_issuable_form.vue
View file @
9652f792
...
...
@@ -31,6 +31,10 @@ export default {
required
:
false
,
default
:
false
,
},
pathIdSeparator
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
...
...
@@ -135,6 +139,7 @@ export default {
:display-reference=
"reference"
:can-remove=
"true"
:is-condensed=
"true"
:path-id-separator=
"pathIdSeparator"
event-namespace=
"pendingIssuable"
/>
</li>
...
...
ee/app/assets/javascripts/related_issues/components/issue_item.vue
View file @
9652f792
<
script
>
import
{
__
}
from
'
~/locale
'
;
import
{
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
IssueMilestone
from
'
~/vue_shared/components/issue/issue_milestone.vue
'
;
import
IssueAssignees
from
'
~/vue_shared/components/issue/issue_assignees.vue
'
;
import
IssueDueDate
from
'
~/boards/components/issue_due_date.vue
'
;
import
IssueWeight
from
'
ee/boards/components/issue_card_weight.vue
'
;
import
relatedIssueMixin
from
'
../mixins/related_issues_mixin
'
;
export
default
{
name
:
'
IssueItem
'
,
components
:
{
IssueMilestone
,
IssueDueDate
,
IssueAssignees
,
IssueWeight
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
mixins
:
[
relatedIssueMixin
],
props
:
{
canReorder
:
{
...
...
@@ -14,7 +28,14 @@ export default {
},
computed
:
{
stateTitle
()
{
return
this
.
isOpen
?
__
(
'
Open
'
)
:
__
(
'
Closed
'
);
return
sprintf
(
'
<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>
'
,
{
state
:
this
.
isOpen
?
__
(
'
Opened
'
)
:
__
(
'
Closed
'
),
timeInWords
:
this
.
isOpen
?
this
.
createdAtInWords
:
this
.
closedAtInWords
,
timestamp
:
this
.
isOpen
?
this
.
createdAtTimestamp
:
this
.
closedAtTimestamp
,
},
);
},
},
};
...
...
@@ -26,22 +47,70 @@ export default {
'issuable-info-container': !canReorder,
'card-body': canReorder,
}"
class="
flex
"
class="
item-body
"
>
<div
class=
"
block-truncated append-right-8 d-inline-flex
"
>
<div
class=
"
block text-secondary append-right-default
"
>
<div
class=
"
item-contents
"
>
<div
class=
"
item-title d-flex align-items-center
"
>
<icon
v-if=
"hasState"
v-tooltip
:css-classes=
"iconClass"
:name=
"iconName"
:size=
"1
2
"
:size=
"1
6
"
:title=
"stateTitle"
:aria-label=
"state"
data-html=
"true"
/>
<icon
v-if=
"confidential"
v-gl-tooltip
name=
"eye-slash"
:size=
"16"
:title=
"__('Confidential')"
class=
"confidential-icon append-right-4"
:aria-label=
"__('Confidential')"
/>
<a
:href=
"computedPath"
class=
"sortable-link"
>
{{
title
}}
</a>
</div>
<div
class=
"item-meta"
>
<div
class=
"d-flex align-items-center item-path-id"
>
<icon
v-if=
"hasState"
v-tooltip
:css-classes=
"iconClass"
:name=
"iconName"
:size=
"16"
:title=
"stateTitle"
:aria-label=
"state"
data-html=
"true"
/>
<span
v-tooltip
:title=
"itemPath"
class=
"path-id-text"
>
{{
itemPath
}}
</span>
{{
pathIdSeparator
}}{{
itemId
}}
</div>
<div
class=
"item-meta-child d-flex align-items-center"
>
<issue-milestone
v-if=
"milestone"
:milestone=
"milestone"
class=
"d-flex align-items-center item-milestone"
/>
<issue-due-date
v-if=
"dueDate"
:date=
"dueDate"
tooltip-placement=
"top"
css-class=
"item-due-date d-flex align-items-center"
/>
<issue-weight
v-if=
"weight"
:weight=
"weight"
class=
"item-weight d-flex align-items-center"
/>
</div>
<issue-assignees
v-if=
"assignees.length"
:assignees=
"assignees"
class=
"item-assignees d-inline-flex"
/>
{{
displayReference
}}
</div>
<a
:href=
"computedPath"
class=
"issue-token-title-text sortable-link"
>
{{
title
}}
</a>
</div>
<button
v-if=
"canRemove"
...
...
@@ -49,13 +118,12 @@ export default {
v-tooltip
:disabled=
"removeDisabled"
type=
"button"
class=
"btn btn-default js-issue-item-remove-button issue-item-remove-button flex-align-self-center flex-right
qa-remove-issue-button"
class=
"btn btn-default btn-svg btn-item-remove js-issue-item-remove-button qa-remove-issue-button"
title=
"Remove"
aria-label=
"Remove"
@
click=
"onRemoveRequest"
>
<i
class=
"fa fa-times"
aria-hidden=
"true"
>
</i
>
<i
con
:size=
"16"
class=
"btn-item-remove-icon"
name=
"close"
/
>
</button>
</div>
</
template
>
ee/app/assets/javascripts/related_issues/components/related_issues_block.vue
View file @
9652f792
...
...
@@ -60,6 +60,11 @@ export default {
required
:
false
,
default
:
''
,
},
pathIdSeparator
:
{
type
:
String
,
required
:
false
,
default
:
'
#
'
,
},
helpPath
:
{
type
:
String
,
required
:
false
,
...
...
@@ -148,16 +153,13 @@ export default {
{{
title
}}
<a
v-if=
"hasHelpPath"
:href=
"helpPath"
>
<i
class=
"related-issues-header-help-icon
fa fa-question-circle"
class=
"related-issues-header-help-icon fa fa-question-circle"
aria-label=
"Read more about related issues"
>
</i>
></i>
</a>
<div
class=
"d-inline-flex lh-100 align-middle"
>
<div
class=
"js-related-issues-header-issue-count
related-issues-header-issue-count issue-count-badge mx-1"
class=
"js-related-issues-header-issue-count related-issues-header-issue-count issue-count-badge mx-1"
>
<span
class=
"issue-count-badge-count"
>
<icon
name=
"issues"
class=
"mr-1 text-secondary"
/>
{{
badgeLabel
}}
...
...
@@ -167,13 +169,12 @@ fa fa-question-circle"
v-if=
"canAdmin"
ref=
"issueCountBadgeAddButton"
type=
"button"
class=
"js-issue-count-badge-add-button
issue-count-badge-add-button btn btn-sm btn-default qa-add-issues-button"
class=
"js-issue-count-badge-add-button issue-count-badge-add-button btn btn-sm btn-default qa-add-issues-button"
aria-label=
"Add an issue"
data-placement=
"top"
@
click=
"toggleAddRelatedIssuesForm"
>
<i
class=
"fa fa-plus"
aria-hidden=
"true"
>
</i>
<i
class=
"fa fa-plus"
aria-hidden=
"true"
></i>
</button>
</div>
</h3>
...
...
@@ -190,6 +191,7 @@ issue-count-badge-add-button btn btn-sm btn-default qa-add-issues-button"
:input-value=
"inputValue"
:pending-references=
"pendingReferences"
:auto-complete-sources=
"autoCompleteSources"
:path-id-separator=
"pathIdSeparator"
/>
</div>
<div
...
...
@@ -206,7 +208,7 @@ issue-count-badge-add-button btn btn-sm btn-default qa-add-issues-button"
class=
"prepend-top-5"
/>
</div>
<ul
ref=
"list"
:class=
"
{ 'content-list': !canReorder }" class="
flex-list issuable
-list">
<ul
ref=
"list"
:class=
"
{ 'content-list': !canReorder }" class="
related-items
-list">
<li
v-for=
"issue in relatedIssues"
:key=
"issue.id"
...
...
@@ -217,16 +219,24 @@ issue-count-badge-add-button btn btn-sm btn-default qa-add-issues-button"
}"
:data-key="issue.id"
:data-epic-issue-id="issue.epic_issue_id"
class="js-related-issues-token-list-item
related-issues-
list-item pt-0 pb-0"
class="js-related-issues-token-list-item list-item pt-0 pb-0"
>
<issue-item
:id-key=
"issue.id"
:display-reference=
"issue.reference"
:confidential=
"issue.confidential"
:title=
"issue.title"
:path=
"issue.path"
:state=
"issue.state"
:milestone=
"issue.milestone"
:due-date=
"issue.due_date"
:assignees=
"issue.assignees"
:weight=
"issue.weight"
:created-at=
"issue.created_at"
:closed-at=
"issue.closed_at"
:can-remove=
"canAdmin"
:can-reorder=
"canReorder"
:path-id-separator=
"pathIdSeparator"
event-namespace=
"relatedIssue"
/>
</li>
...
...
ee/app/assets/javascripts/related_issues/components/related_issues_root.vue
View file @
9652f792
...
...
@@ -246,6 +246,7 @@ export default {
:input-value=
"inputValue"
:auto-complete-sources=
"autoCompleteSources"
:title=
"title"
path-id-separator=
"#"
@
saveReorder=
"saveIssueOrder"
/>
</
template
>
ee/app/assets/javascripts/related_issues/mixins/related_issues_mixin.js
View file @
9652f792
import
{
formatDate
}
from
'
~/lib/utils/datetime_utility
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
icon
from
'
~/vue_shared/components/icon.vue
'
;
import
timeagoMixin
from
'
~/vue_shared/mixins/timeago
'
;
import
eventHub
from
'
../event_hub
'
;
const
mixins
=
{
...
...
@@ -17,11 +19,20 @@ const mixins = {
type
:
String
,
required
:
true
,
},
pathIdSeparator
:
{
type
:
String
,
required
:
true
,
},
eventNamespace
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
confidential
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
title
:
{
type
:
String
,
required
:
false
,
...
...
@@ -37,6 +48,36 @@ const mixins = {
required
:
false
,
default
:
''
,
},
createdAt
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
closedAt
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
milestone
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
dueDate
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
assignees
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
weight
:
{
type
:
Number
,
required
:
false
,
default
:
0
,
},
canRemove
:
{
type
:
Boolean
,
required
:
false
,
...
...
@@ -49,6 +90,7 @@ const mixins = {
directives
:
{
tooltip
,
},
mixins
:
[
timeagoMixin
],
computed
:
{
hasState
()
{
return
this
.
state
&&
this
.
state
.
length
>
0
;
...
...
@@ -63,7 +105,7 @@ const mixins = {
return
this
.
title
.
length
>
0
;
},
iconName
()
{
return
this
.
isOpen
?
'
issue-open
'
:
'
issue-close
'
;
return
this
.
isOpen
?
'
issue-open
-m
'
:
'
issue-close
'
;
},
iconClass
()
{
return
this
.
isOpen
?
'
issue-token-state-icon-open
'
:
'
issue-token-state-icon-closed
'
;
...
...
@@ -74,6 +116,24 @@ const mixins = {
computedPath
()
{
return
this
.
path
.
length
?
this
.
path
:
null
;
},
itemPath
()
{
return
this
.
displayReference
.
split
(
this
.
pathIdSeparator
)[
0
];
},
itemId
()
{
return
this
.
displayReference
.
split
(
this
.
pathIdSeparator
).
pop
();
},
createdAtInWords
()
{
return
this
.
createdAt
?
this
.
timeFormated
(
this
.
createdAt
)
:
''
;
},
createdAtTimestamp
()
{
return
this
.
createdAt
?
formatDate
(
new
Date
(
this
.
createdAt
))
:
''
;
},
closedAtInWords
()
{
return
this
.
closedAt
?
this
.
timeFormated
(
this
.
closedAt
)
:
''
;
},
closedAtTimestamp
()
{
return
this
.
closedAt
?
formatDate
(
new
Date
(
this
.
closedAt
))
:
''
;
},
},
methods
:
{
onRemoveRequest
()
{
...
...
ee/app/assets/stylesheets/components/related_items_list.scss
0 → 100644
View file @
9652f792
$item-path-max-width
:
160px
;
$item-milestone-max-width
:
120px
;
$item-weight-max-width
:
48px
;
.related-items-list
{
padding
:
$gl-padding-4
;
&
,
.list-item
:last-child
{
margin-bottom
:
0
;
}
}
.item-body
{
display
:
flex
;
position
:
relative
;
align-items
:
center
;
padding
:
$gl-padding-8
;
line-height
:
$gl-line-height
;
.item-contents
{
display
:
flex
;
align-items
:
center
;
flex-wrap
:
wrap
;
flex-grow
:
1
;
}
.issue-token-state-icon-open
,
.issue-token-state-icon-closed
,
.confidential-icon
,
.item-milestone
.icon
,
.item-weight
.board-card-info-icon
{
min-width
:
$gl-padding
;
cursor
:
help
;
}
.issue-token-state-icon-open
,
.issue-token-state-icon-closed
{
margin-right
:
$gl-padding-4
;
}
.confidential-icon
{
align-self
:
baseline
;
color
:
$orange-600
;
margin-right
:
$gl-padding-4
;
}
.item-title
{
flex-basis
:
100%
;
margin-bottom
:
$gl-padding-8
;
font-size
:
$gl-font-size-small
;
.sortable-link
{
max-width
:
85%
;
}
.issue-token-state-icon-open
,
.issue-token-state-icon-closed
{
display
:
none
;
}
}
.item-meta
{
display
:
flex
;
flex-wrap
:
wrap
;
flex-basis
:
100%
;
font-size
:
$gl-font-size-small
;
color
:
$gl-text-color-secondary
;
.item-meta-child
{
order
:
0
;
display
:
flex
;
flex-wrap
:
wrap
;
flex-basis
:
100%
;
.item-due-date
,
.item-weight
{
margin-left
:
$gl-padding-8
;
}
.item-milestone
,
.item-weight
{
cursor
:
help
;
text-decoration
:
none
;
}
.item-milestone
{
max-width
:
$item-milestone-max-width
;
}
.item-due-date
{
margin-right
:
0
;
}
.item-weight
{
margin-right
:
0
;
max-width
:
$item-weight-max-width
;
}
}
.item-path-id
.path-id-text
,
.item-milestone
.milestone-title
,
.item-due-date
,
.item-weight
.board-card-info-text
{
color
:
$gl-text-color-secondary
;
display
:
inline-block
;
text-overflow
:
ellipsis
;
overflow
:
hidden
;
white-space
:
nowrap
;
}
.item-path-id
{
order
:
1
;
margin-top
:
$gl-padding-4
;
font-size
:
$gl-font-size-xs
;
.path-id-text
{
font-weight
:
$gl-font-weight-bold
;
max-width
:
$item-path-max-width
;
}
.issue-token-state-icon-open
,
.issue-token-state-icon-closed
{
display
:
block
;
}
}
.item-milestone
.ic-clock
{
color
:
$gl-text-color-tertiary
;
margin-right
:
$gl-padding-4
;
}
.item-assignees
{
order
:
2
;
align-self
:
flex-end
;
align-items
:
center
;
margin-left
:
auto
;
.user-avatar-link
{
margin-right
:
-
$gl-padding-4
;
&
:nth-of-type
(
1
)
{
z-index
:
2
;
}
&
:nth-of-type
(
2
)
{
z-index
:
1
;
}
&
:last-child
{
margin-right
:
0
;
}
}
.avatar
{
height
:
$gl-padding
;
width
:
$gl-padding
;
margin-right
:
0
;
vertical-align
:
bottom
;
}
.avatar-counter
{
height
:
$gl-padding
;
border
:
1px
solid
transparent
;
background-color
:
$gl-text-color-tertiary
;
font-weight
:
$gl-font-weight-bold
;
padding
:
0
$gl-padding-4
;
line-height
:
$gl-padding
;
}
}
}
.btn-item-remove
{
position
:
absolute
;
right
:
0
;
top
:
$gl-padding-4
/
2
;
padding
:
$gl-padding-4
;
margin-right
:
$gl-padding-4
/
2
;
line-height
:
0
;
border-color
:
transparent
;
color
:
$gl-text-color-secondary
;
&
:hover
{
color
:
$gl-text-color
;
}
}
}
@include
media-breakpoint-up
(
sm
)
{
.item-body
{
.item-contents
.item-title
.sortable-link
{
max-width
:
90%
;
}
}
}
/* Small devices (landscape phones, 768px and up) */
@include
media-breakpoint-up
(
md
)
{
.item-body
{
.item-contents
{
min-width
:
0
;
.item-title
{
flex-basis
:
unset
;
// 98% because we compensate
// for remove button which is
// positioned absolutely
width
:
95%
;
margin-bottom
:
$gl-padding-4
;
.sortable-link
{
text-overflow
:
ellipsis
;
overflow
:
hidden
;
white-space
:
nowrap
;
max-width
:
100%
;
}
}
.item-meta
{
.item-path-id
{
order
:
0
;
margin-top
:
0
;
}
.item-meta-child
{
flex-basis
:
unset
;
margin-left
:
auto
;
margin-right
:
$gl-padding-4
;
~
.item-assignees
{
margin-left
:
$gl-padding-4
;
}
}
.item-assignees
{
margin-bottom
:
0
;
margin-left
:
0
;
order
:
2
;
}
}
}
.btn-item-remove
{
order
:
1
;
}
}
}
/* Medium devices (desktops, 992px and up) */
@include
media-breakpoint-up
(
lg
)
{
.item-body
{
padding
:
$gl-padding
;
.item-title
{
font-size
:
$gl-font-size
;
}
.item-meta
.item-path-id
{
font-size
:
inherit
;
// Base size given to `item-meta` is `$gl-font-size-small`
}
.issue-token-state-icon-open
,
.issue-token-state-icon-closed
{
margin-right
:
$gl-padding-4
;
}
}
}
/* Large devices (large desktops, 1200px and up) */
@include
media-breakpoint-up
(
xl
)
{
.item-body
{
padding
:
$gl-padding-8
;
padding-left
:
$gl-padding
;
.item-contents
{
flex-wrap
:
nowrap
;
overflow
:
hidden
;
.item-title
{
display
:
flex
;
margin-bottom
:
0
;
min-width
:
0
;
width
:
auto
;
flex-basis
:
unset
;
font-weight
:
$gl-font-weight-normal
;
.sortable-link
{
display
:
block
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
overflow
:
hidden
;
}
.issue-token-state-icon-open
,
.issue-token-state-icon-closed
{
display
:
block
;
margin-right
:
$gl-padding-8
;
}
.confidential-icon
{
align-self
:
auto
;
margin-top
:
0
;
}
}
.item-meta
{
margin-top
:
0
;
justify-content
:
flex-end
;
flex
:
1
;
flex-wrap
:
nowrap
;
.item-path-id
{
order
:
0
;
margin-top
:
0
;
margin-left
:
$gl-padding-8
;
margin-right
:
auto
;
.issue-token-state-icon-open
,
.issue-token-state-icon-closed
{
display
:
none
;
}
}
.item-meta-child
{
margin-left
:
$gl-padding-8
;
flex-wrap
:
nowrap
;
}
.item-assignees
{
flex-grow
:
0
;
margin-top
:
0
;
margin-right
:
$gl-padding-4
;
.avatar
{
height
:
$gl-padding-24
;
width
:
$gl-padding-24
;
}
.avatar-counter
{
height
:
$gl-padding-24
;
line-height
:
$gl-padding-24
;
border-radius
:
$gl-padding-24
;
}
}
}
}
.btn-item-remove
{
position
:
relative
;
align-self
:
center
;
top
:
initial
;
right
:
0
;
margin-right
:
0
;
padding
:
$btn-sm-side-margin
;
&
:hover
{
border-color
:
$border-color
;
}
}
}
}
ee/app/assets/stylesheets/pages/issues/related_issues.scss
View file @
9652f792
...
...
@@ -33,7 +33,6 @@ $token-spacing-bottom: 0.5em;
li
.issuable-info-container
{
padding-left
:
$gl-padding
;
padding-right
:
$gl-padding-4
;
@include
media-breakpoint-down
(
sm
)
{
padding-left
:
$gl-padding-8
;
...
...
ee/app/models/ee/epic.rb
View file @
9652f792
...
...
@@ -219,9 +219,10 @@ module EE
def
update_project_counter_caches
end
def
issues_readable_by
(
current_user
)
def
issues_readable_by
(
current_user
,
preload:
nil
)
related_issues
=
::
Issue
.
select
(
'issues.*, epic_issues.id as epic_issue_id, epic_issues.relative_position'
)
.
joins
(
:epic_issue
)
.
preload
(
preload
)
.
where
(
"epic_issues.epic_id =
#{
id
}
"
)
.
order
(
'epic_issues.relative_position, epic_issues.id'
)
...
...
ee/app/services/epic_issues/list_service.rb
View file @
9652f792
...
...
@@ -7,7 +7,7 @@ module EpicIssues
def
child_issuables
return
[]
unless
issuable
&
.
group
&
.
feature_available?
(
:epics
)
issuable
.
issues_readable_by
(
current_user
)
issuable
.
issues_readable_by
(
current_user
,
preload:
preload_for_collection
)
end
def
relation_path
(
issue
)
...
...
ee/app/services/issuable_links/list_service.rb
View file @
9652f792
...
...
@@ -18,6 +18,10 @@ module IssuableLinks
private
def
preload_for_collection
[{
project: :namespace
},
:assignees
]
end
def
relation_path
(
object
)
raise
NotImplementedError
end
...
...
@@ -30,15 +34,24 @@ module IssuableLinks
project_issue_path
(
object
.
project
,
object
.
iid
)
end
# rubocop: disable CodeReuse/Serializer
def
to_hash
(
object
)
{
id:
object
.
id
,
confidential:
object
.
confidential
,
title:
object
.
title
,
assignees:
UserSerializer
.
new
.
represent
(
object
.
assignees
),
state:
object
.
state
,
milestone:
MilestoneSerializer
.
new
.
represent
(
object
.
milestone
),
weight:
object
.
weight
,
reference:
reference
(
object
),
path:
issuable_path
(
object
),
relation_path:
relation_path
(
object
)
relation_path:
relation_path
(
object
),
due_date:
object
.
due_date
,
created_at:
object
.
created_at
&
.
to_s
,
closed_at:
object
.
closed_at
}
end
# rubocop: enable CodeReuse/Serializer
end
end
ee/app/services/issue_links/list_service.rb
View file @
9652f792
...
...
@@ -7,7 +7,7 @@ module IssueLinks
private
def
child_issuables
issuable
.
related_issues
(
current_user
,
preload:
{
project: :namespace
}
)
issuable
.
related_issues
(
current_user
,
preload:
preload_for_collection
)
end
def
relation_path
(
issue
)
...
...
ee/changelogs/unreleased/issue_6086.yml
0 → 100644
View file @
9652f792
---
title
:
Epic issue list and related issue list re-design
merge_request
:
author
:
type
:
changed
ee/spec/controllers/groups/epic_issues_controller_spec.rb
View file @
9652f792
...
...
@@ -3,9 +3,10 @@ require 'spec_helper'
describe
Groups
::
EpicIssuesController
do
let
(
:group
)
{
create
(
:group
,
:public
)
}
let
(
:project
)
{
create
(
:project
,
:public
,
group:
group
)
}
let
(
:milestone
)
{
create
(
:milestone
,
project:
project
)
}
let
(
:epic
)
{
create
(
:epic
,
group:
group
)
}
let
(
:
issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:
user
)
{
create
(
:user
)
}
let
(
:
user
)
{
create
(
:user
)
}
let
(
:
issue
)
{
create
(
:issue
,
project:
project
,
milestone:
milestone
,
assignees:
[
user
]
)
}
before
do
stub_licensed_features
(
epics:
true
)
...
...
@@ -45,18 +46,7 @@ describe Groups::EpicIssuesController do
end
it
'returns the correct json'
do
expected_result
=
[
{
'id'
=>
issue
.
id
,
'title'
=>
issue
.
title
,
'state'
=>
issue
.
state
,
'reference'
=>
"
#{
project
.
full_path
}
#
#{
issue
.
iid
}
"
,
'path'
=>
"/
#{
project
.
full_path
}
/issues/
#{
issue
.
iid
}
"
,
'relation_path'
=>
"/groups/
#{
group
.
full_path
}
/-/epics/
#{
epic
.
iid
}
/issues/
#{
epic_issue
.
id
}
"
,
'epic_issue_id'
=>
epic_issue
.
id
}
]
expect
(
JSON
.
parse
(
response
.
body
)).
to
eq
(
expected_result
)
expect
(
JSON
.
parse
(
response
.
body
)).
to
match_schema
(
'related_issues'
,
dir:
'ee'
)
end
end
end
...
...
ee/spec/features/epics/epic_issues_spec.rb
View file @
9652f792
...
...
@@ -32,7 +32,7 @@ describe 'Epic Issues', :js do
end
it
'user can see issues from public project but cannot delete the associations'
do
within
(
'.related-issues-block ul.
issuable
-list'
)
do
within
(
'.related-issues-block ul.
related-items
-list'
)
do
expect
(
page
).
to
have_selector
(
'li'
,
count:
1
)
expect
(
page
).
to
have_content
(
public_issue
.
title
)
expect
(
page
).
not_to
have_selector
(
'button.js-issue-item-remove-button'
)
...
...
@@ -70,7 +70,7 @@ describe 'Epic Issues', :js do
end
it
'user can see all issues of the group and delete the associations'
do
within
(
'.related-issues-block ul.
issuable
-list'
)
do
within
(
'.related-issues-block ul.
related-items
-list'
)
do
expect
(
page
).
to
have_selector
(
'li'
,
count:
2
)
expect
(
page
).
to
have_content
(
public_issue
.
title
)
expect
(
page
).
to
have_content
(
private_issue
.
title
)
...
...
@@ -80,7 +80,7 @@ describe 'Epic Issues', :js do
wait_for_requests
within
(
'.related-issues-block ul.
issuable
-list'
)
do
within
(
'.related-issues-block ul.
related-items
-list'
)
do
expect
(
page
).
to
have_selector
(
'li'
,
count:
1
)
end
end
...
...
@@ -100,7 +100,7 @@ describe 'Epic Issues', :js do
expect
(
page
).
not_to
have_selector
(
'.content-wrapper .alert-wrapper .flash-text'
)
expect
(
page
).
not_to
have_content
(
'No Issue found for given params'
)
within
(
'.related-issues-block ul.
issuable
-list'
)
do
within
(
'.related-issues-block ul.
related-items
-list'
)
do
expect
(
page
).
to
have_selector
(
'li'
,
count:
3
)
expect
(
page
).
to
have_content
(
issue_to_add
.
title
)
end
...
...
@@ -110,7 +110,7 @@ describe 'Epic Issues', :js do
expect
(
first
(
'.js-related-issues-token-list-item'
)).
to
have_content
(
public_issue
.
title
)
expect
(
page
.
all
(
'.js-related-issues-token-list-item'
).
last
).
to
have_content
(
private_issue
.
title
)
drag_to
(
selector:
'.
issuable
-list'
,
to_index:
1
)
drag_to
(
selector:
'.
related-items
-list'
,
to_index:
1
)
expect
(
first
(
'.js-related-issues-token-list-item'
)).
to
have_content
(
private_issue
.
title
)
expect
(
page
.
all
(
'.js-related-issues-token-list-item'
).
last
).
to
have_content
(
public_issue
.
title
)
...
...
ee/spec/features/issuables/related_issues_spec.rb
View file @
9652f792
...
...
@@ -258,7 +258,7 @@ describe 'Related issues', :js do
wait_for_requests
items
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items
=
all
(
'.
item-title a
'
)
# Form gets hidden after submission
expect
(
page
).
not_to
have_selector
(
'.js-add-related-issues-form-area'
)
...
...
@@ -275,7 +275,7 @@ describe 'Related issues', :js do
wait_for_requests
items
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items
=
all
(
'.
item-title a
'
)
expect
(
items
.
count
).
to
eq
(
1
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_project_b_a
.
title
)
...
...
@@ -289,7 +289,7 @@ describe 'Related issues', :js do
wait_for_requests
items
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items
=
all
(
'.
item-title a
'
)
expect
(
items
.
count
).
to
eq
(
1
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_project_b_a
.
title
)
...
...
@@ -311,7 +311,7 @@ describe 'Related issues', :js do
end
it
'shows related issues'
do
items
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items
=
all
(
'.
item-title a
'
)
expect
(
items
.
count
).
to
eq
(
2
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
...
...
@@ -319,7 +319,7 @@ describe 'Related issues', :js do
end
it
'allows us to remove a related issues'
do
items_before
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items_before
=
all
(
'.
item-title a
'
)
expect
(
items_before
.
count
).
to
eq
(
2
)
...
...
@@ -327,7 +327,7 @@ describe 'Related issues', :js do
wait_for_requests
items_after
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items_after
=
all
(
'.
item-title a
'
)
expect
(
items_after
.
count
).
to
eq
(
1
)
end
...
...
@@ -339,7 +339,7 @@ describe 'Related issues', :js do
wait_for_requests
items
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items
=
all
(
'.
item-title a
'
)
expect
(
items
.
count
).
to
eq
(
3
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
...
...
@@ -355,7 +355,7 @@ describe 'Related issues', :js do
wait_for_requests
items
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items
=
all
(
'.
item-title a
'
)
expect
(
items
.
count
).
to
eq
(
2
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
...
...
@@ -370,7 +370,7 @@ describe 'Related issues', :js do
wait_for_requests
items
=
all
(
'.
js-related-issues-token-list-item .issue-token-title-text
'
)
items
=
all
(
'.
item-title a
'
)
expect
(
items
.
count
).
to
eq
(
2
)
expect
(
items
[
0
].
text
).
to
eq
(
issue_b
.
title
)
...
...
ee/spec/fixtures/api/schemas/related_issue.json
0 → 100644
View file @
9652f792
{
"type"
:
"object"
,
"additionalProperties"
:
false
,
"required"
:
[
"id"
,
"confidential"
,
"title"
,
"assignees"
,
"milestone"
,
"due_date"
,
"state"
,
"reference"
,
"path"
,
"relation_path"
,
"weight"
],
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"confidential"
:
{
"type"
:
"boolean"
},
"title"
:
{
"type"
:
"string"
},
"assignees"
:
{
"type"
:
"array"
},
"milestone"
:
{
"type"
:
[
"object"
,
"null"
]
},
"due_date"
:
{
"type"
:
[
"string"
,
"null"
]
},
"state"
:
{
"type"
:
"string"
},
"weight"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"reference"
:
{
"type"
:
"string"
},
"path"
:
{
"type"
:
"string"
},
"relation_path"
:
{
"type"
:
"string"
},
"epic_issue_id"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"created_at"
:
{
"type"
:
"string"
},
"closed_at"
:
{
"type"
:
[
"string"
,
"null"
]
}
}
}
ee/spec/fixtures/api/schemas/related_issues.json
0 → 100644
View file @
9652f792
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"related_issue.json"
}
}
ee/spec/javascripts/boards/issue_card_spec.js
View file @
9652f792
...
...
@@ -48,6 +48,6 @@ describe('Issue card component', () => {
const
el
=
vm
.
$el
.
querySelector
(
'
.board-card-weight
'
);
expect
(
el
).
not
.
toBeNull
();
expect
(
el
.
textContent
.
trim
()).
to
Be
(
'
2
'
);
expect
(
el
.
textContent
.
trim
()).
to
Contain
(
'
2
'
);
});
});
ee/spec/javascripts/epic/components/epic_body_spec.js
View file @
9652f792
...
...
@@ -55,7 +55,7 @@ describe('EpicBodyComponent', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.related-issues-block
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-related-issues-header-issue-count
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.related-issues-token-body
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.
issuable
-list
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.
related-items
-list
'
)).
not
.
toBeNull
();
});
});
});
ee/spec/javascripts/issuable/related_issues/components/add_issuable_form_spec.js
View file @
9652f792
...
...
@@ -21,6 +21,8 @@ const issuable2 = {
state
:
'
opened
'
,
};
const
pathIdSeparator
=
'
#
'
;
describe
(
'
AddIssuableForm
'
,
()
=>
{
let
AddIssuableForm
;
let
vm
;
...
...
@@ -47,6 +49,7 @@ describe('AddIssuableForm', () => {
propsData
:
{
inputValue
:
''
,
pendingReferences
:
[],
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -63,6 +66,7 @@ describe('AddIssuableForm', () => {
propsData
:
{
inputValue
:
'
foo
'
,
pendingReferences
:
[],
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -81,6 +85,7 @@ describe('AddIssuableForm', () => {
propsData
:
{
inputValue
,
pendingReferences
:
[
issuable1
.
reference
,
issuable2
.
reference
],
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -105,6 +110,7 @@ describe('AddIssuableForm', () => {
inputValue
:
''
,
pendingReferences
:
[
issuable1
.
reference
,
issuable2
.
reference
],
isSubmitting
:
true
,
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -125,6 +131,7 @@ describe('AddIssuableForm', () => {
autoCompleteSources
:
{
issues
:
'
/fake/issues/path
'
,
},
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -144,6 +151,7 @@ describe('AddIssuableForm', () => {
propsData
:
{
inputValue
:
''
,
autoCompleteSources
:
{},
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -185,6 +193,7 @@ describe('AddIssuableForm', () => {
autoCompleteSources
:
{
issues
:
'
/fake/issues/path
'
,
},
pathIdSeparator
,
},
}).
$mount
(
el
);
});
...
...
ee/spec/javascripts/issuable/related_issues/components/issue_item_spec.js
View file @
9652f792
...
...
@@ -2,14 +2,22 @@ import Vue from 'vue';
import
issueItem
from
'
ee/related_issues/components/issue_item.vue
'
;
import
eventHub
from
'
ee/related_issues/event_hub
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
defaultMilestone
,
defaultAssignees
}
from
'
../mock_data
'
;
describe
(
'
issueItem
'
,
()
=>
{
let
vm
;
const
props
=
{
idKey
:
1
,
displayReference
:
'
#1
'
,
displayReference
:
'
gitlab-org/gitlab-test#1
'
,
pathIdSeparator
:
'
#
'
,
path
:
`
${
gl
.
TEST_HOST
}
/path`
,
title
:
'
title
'
,
confidential
:
true
,
dueDate
:
'
2018-12-31
'
,
weight
:
10
,
createdAt
:
'
2018-12-01T00:00:00.00Z
'
,
milestone
:
defaultMilestone
,
assignees
:
defaultAssignees
,
};
beforeEach
(()
=>
{
...
...
@@ -22,12 +30,6 @@ describe('issueItem', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.issuable-info-container
'
)).
toBeNull
();
});
it
(
'
renders displayReference
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.text-secondary
'
).
innerText
.
trim
()).
toEqual
(
props
.
displayReference
,
);
});
it
(
'
does not render token state
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.text-secondary svg
'
)).
toBeNull
();
});
...
...
@@ -38,11 +40,17 @@ describe('issueItem', () => {
describe
(
'
token title
'
,
()
=>
{
it
(
'
links to computedPath
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
a
'
).
href
).
toEqual
(
props
.
path
);
expect
(
vm
.
$el
.
querySelector
(
'
.item-title a
'
).
href
).
toEqual
(
props
.
path
);
});
it
(
'
renders confidential icon
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.item-title svg.confidential-icon use
'
).
getAttribute
(
'
xlink:href
'
),
).
toContain
(
'
eye-slash
'
);
});
it
(
'
renders title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
a
'
).
innerText
.
trim
()).
toEqual
(
props
.
title
);
expect
(
vm
.
$el
.
querySelector
(
'
.item-title
a
'
).
innerText
.
trim
()).
toEqual
(
props
.
title
);
});
});
...
...
@@ -52,7 +60,7 @@ describe('issueItem', () => {
beforeEach
(
done
=>
{
vm
.
state
=
'
opened
'
;
Vue
.
nextTick
(()
=>
{
tokenState
=
vm
.
$el
.
querySelector
(
'
.
text-secondary
svg
'
);
tokenState
=
vm
.
$el
.
querySelector
(
'
.
item-meta
svg
'
);
done
();
});
});
...
...
@@ -62,7 +70,12 @@ describe('issueItem', () => {
});
it
(
'
renders state title
'
,
()
=>
{
expect
(
tokenState
.
getAttribute
(
'
data-original-title
'
)).
toEqual
(
'
Open
'
);
const
stateTitle
=
tokenState
.
getAttribute
(
'
data-original-title
'
).
trim
();
expect
(
stateTitle
).
toContain
(
'
<span class="bold">Opened</span>
'
);
expect
(
stateTitle
).
toContain
(
'
<span class="text-tertiary">Dec 1, 2018 12:00am GMT+0000</span>
'
,
);
});
it
(
'
renders aria label
'
,
()
=>
{
...
...
@@ -75,6 +88,7 @@ describe('issueItem', () => {
it
(
'
renders close icon when close state
'
,
done
=>
{
vm
.
state
=
'
closed
'
;
vm
.
closedAt
=
'
2018-12-01T00:00:00.00Z
'
;
Vue
.
nextTick
(()
=>
{
expect
(
tokenState
.
classList
.
contains
(
'
issue-token-state-icon-closed
'
)).
toEqual
(
true
);
...
...
@@ -83,6 +97,57 @@ describe('issueItem', () => {
});
});
describe
(
'
token metadata
'
,
()
=>
{
let
tokenMetadata
;
beforeEach
(
done
=>
{
Vue
.
nextTick
(()
=>
{
tokenMetadata
=
vm
.
$el
.
querySelector
(
'
.item-meta
'
);
done
();
});
});
it
(
'
renders item path and ID
'
,
()
=>
{
const
pathAndID
=
tokenMetadata
.
querySelector
(
'
.item-path-id
'
).
innerText
.
trim
();
expect
(
pathAndID
).
toContain
(
'
gitlab-org/gitlab-test
'
);
expect
(
pathAndID
).
toContain
(
'
#1
'
);
});
it
(
'
renders milestone icon and name
'
,
()
=>
{
const
milestoneIconEl
=
tokenMetadata
.
querySelector
(
'
.item-milestone svg use
'
);
const
milestoneTitle
=
tokenMetadata
.
querySelector
(
'
.item-milestone .milestone-title
'
);
expect
(
milestoneIconEl
.
getAttribute
(
'
xlink:href
'
)).
toContain
(
'
clock
'
);
expect
(
milestoneTitle
.
innerText
.
trim
()).
toContain
(
'
Milestone title
'
);
});
it
(
'
renders date icon and due date
'
,
()
=>
{
const
dueDateIconEl
=
tokenMetadata
.
querySelector
(
'
.item-due-date svg use
'
);
const
dueDateEl
=
tokenMetadata
.
querySelector
(
'
.item-due-date time
'
);
expect
(
dueDateIconEl
.
getAttribute
(
'
xlink:href
'
)).
toContain
(
'
calendar
'
);
expect
(
dueDateEl
.
innerText
.
trim
()).
toContain
(
'
Dec 31
'
);
});
it
(
'
renders weight icon and value
'
,
()
=>
{
const
dueDateIconEl
=
tokenMetadata
.
querySelector
(
'
.item-weight svg use
'
);
const
dueDateEl
=
tokenMetadata
.
querySelector
(
'
.item-weight span
'
);
expect
(
dueDateIconEl
.
getAttribute
(
'
xlink:href
'
)).
toContain
(
'
weight
'
);
expect
(
dueDateEl
.
innerText
.
trim
()).
toContain
(
'
10
'
);
});
});
describe
(
'
token assignees
'
,
()
=>
{
it
(
'
renders assignees avatars
'
,
()
=>
{
const
assigneesEl
=
vm
.
$el
.
querySelector
(
'
.item-assignees
'
);
expect
(
assigneesEl
.
querySelectorAll
(
'
.user-avatar-link
'
).
length
).
toBe
(
2
);
expect
(
assigneesEl
.
querySelector
(
'
.avatar-counter
'
).
innerText
.
trim
()).
toContain
(
'
+2
'
);
});
});
describe
(
'
remove button
'
,
()
=>
{
let
removeBtn
;
...
...
ee/spec/javascripts/issuable/related_issues/components/issue_token_spec.js
View file @
9652f792
...
...
@@ -6,6 +6,7 @@ describe('IssueToken', () => {
const
idKey
=
200
;
const
displayReference
=
'
foo/bar#123
'
;
const
title
=
'
some title
'
;
const
pathIdSeparator
=
'
#
'
;
let
IssueToken
;
let
vm
;
...
...
@@ -25,6 +26,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -45,6 +47,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
title
,
},
}).
$mount
();
...
...
@@ -63,6 +66,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
title
,
path
,
},
...
...
@@ -81,6 +85,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
state
:
'
opened
'
,
},
}).
$mount
();
...
...
@@ -97,6 +102,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
state
:
'
reopened
'
,
},
}).
$mount
();
...
...
@@ -113,6 +119,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
state
:
'
closed
'
,
},
}).
$mount
();
...
...
@@ -131,6 +138,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
title
,
state
,
},
...
...
@@ -153,6 +161,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
},
}).
$mount
();
});
...
...
@@ -168,6 +177,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
canRemove
:
true
,
},
}).
$mount
();
...
...
@@ -187,6 +197,7 @@ describe('IssueToken', () => {
propsData
:
{
idKey
,
displayReference
,
pathIdSeparator
,
},
}).
$mount
();
removeRequestSpy
=
jasmine
.
createSpy
(
'
spy
'
);
...
...
ee/spec/javascripts/issuable/related_issues/mock_data.js
View file @
9652f792
...
...
@@ -7,6 +7,7 @@ export const defaultProps = {
export
const
issuable1
=
{
id
:
200
,
epic_issue_id
:
1
,
confidential
:
false
,
reference
:
'
foo/bar#123
'
,
displayReference
:
'
#123
'
,
title
:
'
some title
'
,
...
...
@@ -17,6 +18,7 @@ export const issuable1 = {
export
const
issuable2
=
{
id
:
201
,
epic_issue_id
:
2
,
confidential
:
false
,
reference
:
'
foo/bar#124
'
,
displayReference
:
'
#124
'
,
title
:
'
some other thing
'
,
...
...
@@ -27,6 +29,7 @@ export const issuable2 = {
export
const
issuable3
=
{
id
:
202
,
epic_issue_id
:
3
,
confidential
:
false
,
reference
:
'
foo/bar#125
'
,
displayReference
:
'
#125
'
,
title
:
'
some other other thing
'
,
...
...
@@ -37,6 +40,7 @@ export const issuable3 = {
export
const
issuable4
=
{
id
:
203
,
epic_issue_id
:
4
,
confidential
:
false
,
reference
:
'
foo/bar#126
'
,
displayReference
:
'
#126
'
,
title
:
'
some other other other thing
'
,
...
...
@@ -47,9 +51,61 @@ export const issuable4 = {
export
const
issuable5
=
{
id
:
204
,
epic_issue_id
:
5
,
confidential
:
false
,
reference
:
'
foo/bar#127
'
,
displayReference
:
'
#127
'
,
title
:
'
some other other other thing
'
,
path
:
'
/foo/bar/issues/127
'
,
state
:
'
opened
'
,
};
export
const
defaultMilestone
=
{
id
:
1
,
state
:
'
active
'
,
title
:
'
Milestone title
'
,
start_date
:
'
2018-01-01
'
,
due_date
:
'
2019-12-31
'
,
};
export
const
defaultAssignees
=
[
{
id
:
1
,
name
:
'
Administrator
'
,
username
:
'
root
'
,
state
:
'
active
'
,
avatar_url
:
`
${
gl
.
TEST_HOST
}
`
,
web_url
:
`
${
gl
.
TEST_HOST
}
/root`
,
status_tooltip_html
:
null
,
path
:
'
/root
'
,
},
{
id
:
13
,
name
:
'
Brooks Beatty
'
,
username
:
'
brynn_champlin
'
,
state
:
'
active
'
,
avatar_url
:
`
${
gl
.
TEST_HOST
}
`
,
web_url
:
`
${
gl
.
TEST_HOST
}
/brynn_champlin`
,
status_tooltip_html
:
null
,
path
:
'
/brynn_champlin
'
,
},
{
id
:
6
,
name
:
'
Bryce Turcotte
'
,
username
:
'
melynda
'
,
state
:
'
active
'
,
avatar_url
:
`
${
gl
.
TEST_HOST
}
`
,
web_url
:
`
${
gl
.
TEST_HOST
}
/melynda`
,
status_tooltip_html
:
null
,
path
:
'
/melynda
'
,
},
{
id
:
20
,
name
:
'
Conchita Eichmann
'
,
username
:
'
juliana_gulgowski
'
,
state
:
'
active
'
,
avatar_url
:
`
${
gl
.
TEST_HOST
}
`
,
web_url
:
`
${
gl
.
TEST_HOST
}
/juliana_gulgowski`
,
status_tooltip_html
:
null
,
path
:
'
/juliana_gulgowski
'
,
},
];
ee/spec/services/epic_issues/list_service_spec.rb
View file @
9652f792
...
...
@@ -7,7 +7,7 @@ describe EpicIssues::ListService do
let
(
:other_project
)
{
create
(
:project_empty_repo
,
group:
group
)
}
let
(
:epic
)
{
create
(
:epic
,
group:
group
)
}
let
(
:issue1
)
{
create
:issue
,
project:
project
}
let
(
:issue1
)
{
create
:issue
,
project:
project
,
weight:
1
}
let
(
:issue2
)
{
create
:issue
,
project:
project
}
let
(
:issue3
)
{
create
:issue
,
project:
other_project
}
...
...
@@ -31,6 +31,36 @@ describe EpicIssues::ListService do
stub_licensed_features
(
epics:
true
)
end
it
'does not have N+1 queries'
,
:use_clean_rails_memory_store_caching
,
:request_store
do
# The control query is made with the worst case scenario:
# * Two different issues from two different projects that belong to two different groups
# Then a new group with a new project is created and we do the call again to check if there will be no
# additional queries.
group
.
add_developer
(
user
)
list_service
=
described_class
.
new
(
epic
,
user
)
new_group
=
create
(
:group
,
:private
)
new_group
.
add_developer
(
user
)
new_project
=
create
(
:project
,
namespace:
new_group
)
milestone
=
create
(
:milestone
,
project:
project
)
milestone2
=
create
(
:milestone
,
project:
new_project
)
new_issue1
=
create
(
:issue
,
project:
project
,
milestone:
milestone
,
assignees:
[
user
])
new_issue3
=
create
(
:issue
,
project:
new_project
,
milestone:
milestone2
)
create
(
:epic_issue
,
issue:
new_issue1
,
epic:
epic
,
relative_position:
3
)
create
(
:epic_issue
,
issue:
new_issue3
,
epic:
epic
,
relative_position:
5
)
control_count
=
ActiveRecord
::
QueryRecorder
.
new
{
list_service
.
execute
}.
count
new_group2
=
create
(
:group
,
:private
)
new_project2
=
create
(
:project
,
namespace:
new_group2
)
new_group2
.
add_developer
(
user
)
milestone3
=
create
(
:milestone
,
project:
new_project2
)
new_issue4
=
create
(
:issue
,
project:
new_project
,
milestone:
milestone3
)
create
(
:epic_issue
,
issue:
new_issue4
,
epic:
epic
,
relative_position:
6
)
expect
{
list_service
.
execute
}.
not_to
exceed_query_limit
(
control_count
)
end
context
'owner can see all issues and destroy their associations'
do
before
do
group
.
add_developer
(
user
)
...
...
@@ -41,31 +71,53 @@ describe EpicIssues::ListService do
{
id:
issue2
.
id
,
title:
issue2
.
title
,
assignees:
[],
state:
issue2
.
state
,
milestone:
nil
,
weight:
nil
,
confidential:
false
,
reference:
issue2
.
to_reference
(
full:
true
),
path:
"/
#{
project
.
full_path
}
/issues/
#{
issue2
.
iid
}
"
,
relation_path:
"/groups/
#{
group
.
full_path
}
/-/epics/
#{
epic
.
iid
}
/issues/
#{
epic_issue2
.
id
}
"
,
epic_issue_id:
epic_issue2
.
id
epic_issue_id:
epic_issue2
.
id
,
due_date:
nil
,
created_at:
issue2
.
created_at
.
to_s
,
closed_at:
issue2
.
closed_at
},
{
id:
issue1
.
id
,
title:
issue1
.
title
,
assignees:
[],
state:
issue1
.
state
,
milestone:
nil
,
weight:
1
,
confidential:
false
,
reference:
issue1
.
to_reference
(
full:
true
),
path:
"/
#{
project
.
full_path
}
/issues/
#{
issue1
.
iid
}
"
,
relation_path:
"/groups/
#{
group
.
full_path
}
/-/epics/
#{
epic
.
iid
}
/issues/
#{
epic_issue1
.
id
}
"
,
epic_issue_id:
epic_issue1
.
id
epic_issue_id:
epic_issue1
.
id
,
due_date:
nil
,
created_at:
issue1
.
created_at
.
to_s
,
closed_at:
issue1
.
closed_at
},
{
id:
issue3
.
id
,
title:
issue3
.
title
,
assignees:
[],
state:
issue3
.
state
,
milestone:
nil
,
weight:
nil
,
confidential:
false
,
reference:
issue3
.
to_reference
(
full:
true
),
path:
"/
#{
other_project
.
full_path
}
/issues/
#{
issue3
.
iid
}
"
,
relation_path:
"/groups/
#{
group
.
full_path
}
/-/epics/
#{
epic
.
iid
}
/issues/
#{
epic_issue3
.
id
}
"
,
epic_issue_id:
epic_issue3
.
id
epic_issue_id:
epic_issue3
.
id
,
due_date:
nil
,
created_at:
issue3
.
created_at
.
to_s
,
closed_at:
issue3
.
closed_at
}
]
expect
(
subject
).
to
eq
(
expected_result
)
end
end
...
...
@@ -80,20 +132,34 @@ describe EpicIssues::ListService do
{
id:
issue2
.
id
,
title:
issue2
.
title
,
assignees:
[],
state:
issue2
.
state
,
milestone:
nil
,
weight:
nil
,
confidential:
false
,
reference:
issue2
.
to_reference
(
full:
true
),
path:
"/
#{
project
.
full_path
}
/issues/
#{
issue2
.
iid
}
"
,
relation_path:
nil
,
epic_issue_id:
epic_issue2
.
id
epic_issue_id:
epic_issue2
.
id
,
due_date:
nil
,
created_at:
issue2
.
created_at
.
to_s
,
closed_at:
issue2
.
closed_at
},
{
id:
issue1
.
id
,
title:
issue1
.
title
,
assignees:
[],
state:
issue1
.
state
,
milestone:
nil
,
weight:
1
,
confidential:
false
,
reference:
issue1
.
to_reference
(
full:
true
),
path:
"/
#{
project
.
full_path
}
/issues/
#{
issue1
.
iid
}
"
,
relation_path:
nil
,
epic_issue_id:
epic_issue1
.
id
epic_issue_id:
epic_issue1
.
id
,
due_date:
nil
,
created_at:
issue1
.
created_at
.
to_s
,
closed_at:
issue1
.
closed_at
}
]
...
...
ee/spec/services/issue_links/list_service_spec.rb
View file @
9652f792
...
...
@@ -39,8 +39,9 @@ describe IssueLinks::ListService do
control_count
=
ActiveRecord
::
QueryRecorder
.
new
{
subject
}.
count
project
=
create
:project
,
:public
issue_x
=
create
:issue
,
project:
project
issue_y
=
create
:issue
,
project:
project
milestone
=
create
:milestone
,
project:
project
issue_x
=
create
:issue
,
project:
project
,
milestone:
milestone
issue_y
=
create
:issue
,
project:
project
,
assignees:
[
user
]
issue_z
=
create
:issue
,
project:
project
create
:issue_link
,
source:
issue_x
,
target:
issue_y
create
:issue_link
,
source:
issue_x
,
target:
issue_z
...
...
locale/gitlab.pot
View file @
9652f792
...
...
@@ -1061,6 +1061,9 @@ msgstr ""
msgid "Available specific runners"
msgstr ""
msgid "Avatar for %{assigneeName}"
msgstr ""
msgid "Avatar will be removed. Are you sure?"
msgstr ""
...
...
@@ -3456,6 +3459,9 @@ msgstr ""
msgid "Expiration date"
msgstr ""
msgid "Expired %{expiredOn}"
msgstr ""
msgid "Expires in %{expires_at}"
msgstr ""
...
...
@@ -8196,9 +8202,15 @@ msgstr ""
msgid "Started"
msgstr ""
msgid "Started %{startsIn}"
msgstr ""
msgid "Starting..."
msgstr ""
msgid "Starts %{startsIn}"
msgstr ""
msgid "Starts at (UTC)"
msgstr ""
...
...
spec/javascripts/boards/mock_data.js
View file @
9652f792
...
...
@@ -130,3 +130,12 @@ export const mockAssigneesList = [
path
:
'
/root
'
,
},
];
export
const
mockMilestone
=
{
id
:
1
,
state
:
'
active
'
,
title
:
'
Milestone title
'
,
description
:
'
Harum corporis aut consequatur quae dolorem error sequi quia.
'
,
start_date
:
'
2018-01-01
'
,
due_date
:
'
2019-12-31
'
,
};
spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
0 → 100644
View file @
9652f792
import
Vue
from
'
vue
'
;
import
IssueAssignees
from
'
~/vue_shared/components/issue/issue_assignees.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mockAssigneesList
}
from
'
spec/boards/mock_data
'
;
const
createComponent
=
(
assignees
=
mockAssigneesList
,
cssClass
=
''
)
=>
{
const
Component
=
Vue
.
extend
(
IssueAssignees
);
return
mountComponent
(
Component
,
{
assignees
,
cssClass
,
});
};
describe
(
'
IssueAssigneesComponent
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
data
'
,
()
=>
{
it
(
'
returns default data props
'
,
()
=>
{
expect
(
vm
.
maxVisibleAssignees
).
toBe
(
2
);
expect
(
vm
.
maxAssigneeAvatars
).
toBe
(
3
);
expect
(
vm
.
maxAssignees
).
toBe
(
99
);
});
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
countOverLimit
'
,
()
=>
{
it
(
'
should return difference between assignees count and maxVisibleAssignees
'
,
()
=>
{
expect
(
vm
.
countOverLimit
).
toBe
(
mockAssigneesList
.
length
-
vm
.
maxVisibleAssignees
);
});
});
describe
(
'
assigneesToShow
'
,
()
=>
{
it
(
'
should return assignees containing only 2 items when count more than maxAssigneeAvatars
'
,
()
=>
{
expect
(
vm
.
assigneesToShow
.
length
).
toBe
(
2
);
});
it
(
'
should return all assignees as it is when count less than maxAssigneeAvatars
'
,
()
=>
{
vm
.
assignees
=
mockAssigneesList
.
slice
(
0
,
3
);
// Set 3 Assignees
expect
(
vm
.
assigneesToShow
.
length
).
toBe
(
3
);
});
});
describe
(
'
assigneesCounterTooltip
'
,
()
=>
{
it
(
'
should return string containing count of remaining assignees when count more than maxAssigneeAvatars
'
,
()
=>
{
expect
(
vm
.
assigneesCounterTooltip
).
toBe
(
'
3 more assignees
'
);
});
});
describe
(
'
shouldRenderAssigneesCounter
'
,
()
=>
{
it
(
'
should return `false` when assignees count less than maxAssigneeAvatars
'
,
()
=>
{
vm
.
assignees
=
mockAssigneesList
.
slice
(
0
,
3
);
// Set 3 Assignees
expect
(
vm
.
shouldRenderAssigneesCounter
).
toBe
(
false
);
});
it
(
'
should return `true` when assignees count more than maxAssigneeAvatars
'
,
()
=>
{
expect
(
vm
.
shouldRenderAssigneesCounter
).
toBe
(
true
);
});
});
describe
(
'
assigneeCounterLabel
'
,
()
=>
{
it
(
'
should return count of additional assignees total assignees count more than maxAssigneeAvatars
'
,
()
=>
{
expect
(
vm
.
assigneeCounterLabel
).
toBe
(
'
+3
'
);
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
avatarUrlTitle
'
,
()
=>
{
it
(
'
returns string containing alt text for assignee avatar
'
,
()
=>
{
expect
(
vm
.
avatarUrlTitle
(
mockAssigneesList
[
0
])).
toBe
(
'
Avatar for Terrell Graham
'
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component root element with class `issue-assignees`
'
,
()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
issue-assignees
'
)).
toBe
(
true
);
});
it
(
'
renders assignee avatars
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'
.user-avatar-link
'
).
length
).
toBe
(
2
);
});
it
(
'
renders assignee tooltips
'
,
()
=>
{
const
tooltipText
=
vm
.
$el
.
querySelectorAll
(
'
.user-avatar-link
'
)[
0
]
.
querySelector
(
'
.js-assignee-tooltip
'
).
innerText
;
expect
(
tooltipText
).
toContain
(
'
Assignee
'
);
expect
(
tooltipText
).
toContain
(
'
Terrell Graham
'
);
expect
(
tooltipText
).
toContain
(
'
@monserrate.gleichner
'
);
});
it
(
'
renders additional assignees count
'
,
()
=>
{
const
avatarCounterEl
=
vm
.
$el
.
querySelector
(
'
.avatar-counter
'
);
expect
(
avatarCounterEl
.
innerText
.
trim
()).
toBe
(
'
+3
'
);
expect
(
avatarCounterEl
.
getAttribute
(
'
data-original-title
'
)).
toBe
(
'
3 more assignees
'
);
});
});
});
spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
0 → 100644
View file @
9652f792
import
Vue
from
'
vue
'
;
import
IssueMilestone
from
'
~/vue_shared/components/issue/issue_milestone.vue
'
;
import
mountComponent
from
'
spec/helpers/vue_mount_component_helper
'
;
import
{
mockMilestone
}
from
'
spec/boards/mock_data
'
;
const
createComponent
=
(
milestone
=
mockMilestone
)
=>
{
const
Component
=
Vue
.
extend
(
IssueMilestone
);
return
mountComponent
(
Component
,
{
milestone
,
});
};
describe
(
'
IssueMilestoneComponent
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
isMilestoneStarted
'
,
()
=>
{
it
(
'
should return `false` when milestoneStart prop is not defined
'
,
done
=>
{
const
vmStartUndefined
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
start_date
:
''
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmStartUndefined
.
isMilestoneStarted
).
toBe
(
false
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmStartUndefined
.
$destroy
();
});
it
(
'
should return `true` when milestone start date is past current date
'
,
done
=>
{
const
vmStarted
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
start_date
:
'
1990-07-22
'
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmStarted
.
isMilestoneStarted
).
toBe
(
true
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmStarted
.
$destroy
();
});
});
describe
(
'
isMilestonePastDue
'
,
()
=>
{
it
(
'
should return `false` when milestoneDue prop is not defined
'
,
done
=>
{
const
vmDueUndefined
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
due_date
:
''
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmDueUndefined
.
isMilestonePastDue
).
toBe
(
false
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmDueUndefined
.
$destroy
();
});
it
(
'
should return `true` when milestone due is past current date
'
,
done
=>
{
const
vmPastDue
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
due_date
:
'
1990-07-22
'
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmPastDue
.
isMilestonePastDue
).
toBe
(
true
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmPastDue
.
$destroy
();
});
});
describe
(
'
milestoneDatesAbsolute
'
,
()
=>
{
it
(
'
returns string containing absolute milestone due date
'
,
()
=>
{
expect
(
vm
.
milestoneDatesAbsolute
).
toBe
(
'
(December 31, 2019)
'
);
});
it
(
'
returns string containing absolute milestone start date when due date is not present
'
,
done
=>
{
const
vmDueUndefined
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
due_date
:
''
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmDueUndefined
.
milestoneDatesAbsolute
).
toBe
(
'
(January 1, 2018)
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmDueUndefined
.
$destroy
();
});
it
(
'
returns empty string when both milestone start and due dates are not present
'
,
done
=>
{
const
vmDatesUndefined
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
start_date
:
''
,
due_date
:
''
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmDatesUndefined
.
milestoneDatesAbsolute
).
toBe
(
''
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmDatesUndefined
.
$destroy
();
});
});
describe
(
'
milestoneDatesHuman
'
,
()
=>
{
it
(
'
returns string containing milestone due date when date is yet to be due
'
,
done
=>
{
const
vmFuture
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
due_date
:
`
${
new
Date
().
getFullYear
()
+
10
}
-01-01`
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmFuture
.
milestoneDatesHuman
).
toContain
(
'
years remaining
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmFuture
.
$destroy
();
});
it
(
'
returns string containing milestone start date when date has already started and due date is not present
'
,
done
=>
{
const
vmStarted
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
start_date
:
'
1990-07-22
'
,
due_date
:
''
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmStarted
.
milestoneDatesHuman
).
toContain
(
'
Started
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmStarted
.
$destroy
();
});
it
(
'
returns string containing milestone start date when date is yet to start and due date is not present
'
,
done
=>
{
const
vmStarts
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
start_date
:
`
${
new
Date
().
getFullYear
()
+
10
}
-01-01`
,
due_date
:
''
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmStarts
.
milestoneDatesHuman
).
toContain
(
'
Starts
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmStarts
.
$destroy
();
});
it
(
'
returns empty string when milestone start and due dates are not present
'
,
done
=>
{
const
vmDatesUndefined
=
createComponent
(
Object
.
assign
({},
mockMilestone
,
{
start_date
:
''
,
due_date
:
''
,
}),
);
Vue
.
nextTick
()
.
then
(()
=>
{
expect
(
vmDatesUndefined
.
milestoneDatesHuman
).
toBe
(
''
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
vmDatesUndefined
.
$destroy
();
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component root element with class `issue-milestone-details`
'
,
()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
issue-milestone-details
'
)).
toBe
(
true
);
});
it
(
'
renders milestone icon
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
svg use
'
).
getAttribute
(
'
xlink:href
'
)).
toContain
(
'
clock
'
);
});
it
(
'
renders milestone title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.milestone-title
'
).
innerText
.
trim
()).
toBe
(
mockMilestone
.
title
);
});
it
(
'
renders milestone tooltip
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-item-milestone
'
).
innerText
.
trim
()).
toContain
(
mockMilestone
.
title
,
);
});
});
});
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