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
d1eec7ce
Commit
d1eec7ce
authored
Jun 24, 2020
by
Eulyeon Ko
Committed by
Paul Slaughter
Jun 24, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Allow Hiding/Collapsing of Milestone header on Roadmap
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34357
parent
69b34501
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
480 additions
and
365 deletions
+480
-365
ee/app/assets/javascripts/roadmap/components/epic_item_details.vue
...sets/javascripts/roadmap/components/epic_item_details.vue
+21
-14
ee/app/assets/javascripts/roadmap/components/milestone_timeline.vue
...ets/javascripts/roadmap/components/milestone_timeline.vue
+17
-10
ee/app/assets/javascripts/roadmap/components/milestones_list_section.vue
...avascripts/roadmap/components/milestones_list_section.vue
+71
-4
ee/app/assets/stylesheets/pages/roadmap.scss
ee/app/assets/stylesheets/pages/roadmap.scss
+9
-0
ee/changelogs/unreleased/212494-allow-hiding-collapsing-of-milestone-header-on-roadmap.yml
...llow-hiding-collapsing-of-milestone-header-on-roadmap.yml
+5
-0
ee/spec/frontend/roadmap/components/epic_item_details_spec.js
...pec/frontend/roadmap/components/epic_item_details_spec.js
+217
-265
ee/spec/frontend/roadmap/components/milestone_timeline_spec.js
...ec/frontend/roadmap/components/milestone_timeline_spec.js
+31
-28
ee/spec/frontend/roadmap/components/milestones_list_section_spec.js
...ontend/roadmap/components/milestones_list_section_spec.js
+86
-34
locale/gitlab.pot
locale/gitlab.pot
+9
-4
spec/frontend/helpers/vue_mock_directive.js
spec/frontend/helpers/vue_mock_directive.js
+14
-6
No files found.
ee/app/assets/javascripts/roadmap/components/epic_item_details.vue
View file @
d1eec7ce
...
...
@@ -74,9 +74,7 @@ export default {
if
(
this
.
isEmptyChildrenWithFilter
)
{
return
this
.
infoSearchLabel
;
}
return
this
.
childrenFlags
[
this
.
itemId
].
itemExpanded
?
__
(
'
Collapse child epics
'
)
:
__
(
'
Expand child epics
'
);
return
this
.
childrenFlags
[
this
.
itemId
].
itemExpanded
?
__
(
'
Collapse
'
)
:
__
(
'
Expand
'
);
},
childrenFetchInProgress
()
{
return
this
.
epic
.
hasChildren
&&
this
.
childrenFlags
[
this
.
itemId
].
itemChildrenFetchInProgress
;
...
...
@@ -108,9 +106,12 @@ export default {
</
script
>
<
template
>
<div
class=
"epic-details-cell"
data-qa-selector=
"epic_details_cell"
>
<div
class=
"epic-details-cell gl-display-flex gl-flex-direction-column gl-justify-content-center"
data-qa-selector=
"epic_details_cell"
>
<div
class=
"
d-flex align-items-start p-2
"
class=
"
gl-display-flex align-items-start gl-px-3 gl-mb-1
"
:class=
"[epic.isChildEpic ? childMarginClassname : '']"
>
<span
ref=
"expandCollapseInfo"
>
...
...
@@ -130,38 +131,44 @@ export default {
</gl-button>
</span>
<gl-tooltip
v-if=
"isEmptyChildrenWithFilter"
v-if=
"!isExpandIconHidden"
ref=
"expandIconTooltip"
triggers=
"hover"
:target=
"() => $refs.expandCollapseInfo"
boundary=
"viewport"
offset=
"
80
"
offset=
"
15
"
placement=
"topright"
>
{{
infoSearch
Label
}}
{{
expandIcon
Label
}}
</gl-tooltip>
<div
class=
"overflow-hidden flex-grow-1 mx-2"
>
<a
:href=
"epic.webUrl"
:title=
"epic.title"
class=
"epic-title d-block text-body bold"
>
<a
:href=
"epic.webUrl"
:title=
"epic.title"
class=
"epic-title gl-mt-1 d-block text-body bold"
>
{{
epic
.
title
}}
</a>
<div
class=
"epic-group-timeframe d-flex text-secondary"
>
<
p
<
span
v-if=
"isEpicGroupDifferent && !epic.hasParent"
:title=
"epic.groupFullName"
class=
"epic-group"
>
{{
epic
.
groupName
}}
</
p
>
</
span
>
<span
v-if=
"isEpicGroupDifferent && !epic.hasParent"
class=
"mx-1"
aria-hidden=
"true"
>
·
</span
>
<
p
class=
"epic-timeframe"
:title=
"timeframeString"
>
{{
timeframeString
}}
</p
>
<
span
class=
"epic-timeframe"
:title=
"timeframeString"
>
{{
timeframeString
}}
</span
>
</div>
</div>
<template
v-if=
"allowSubEpics"
>
<div
ref=
"childEpicsCount"
class=
"d-flex text-secondary text-nowrap"
>
<div
ref=
"childEpicsCount"
class=
"
gl-mt-1
d-flex text-secondary text-nowrap"
>
<gl-icon
name=
"epic"
class=
"align-text-bottom mr-1"
aria-hidden=
"true"
/>
<p
class=
"m-0"
:aria-label=
"childEpicsCountText"
>
{{
childEpicsCount
}}
</p>
</div>
<gl-tooltip
:target=
"() => $refs.childEpicsCount"
>
<gl-tooltip
ref=
"childEpicsCountTooltip"
:target=
"() => $refs.childEpicsCount"
>
<span
:class=
"
{ bold: hasFiltersApplied }">
{{
childEpicsCountText
}}
</span>
<span
v-if=
"hasFiltersApplied"
class=
"d-block"
>
{{
childEpicsSearchText
}}
</span>
</gl-tooltip>
...
...
ee/app/assets/javascripts/roadmap/components/milestone_timeline.vue
View file @
d1eec7ce
...
...
@@ -24,6 +24,10 @@ export default {
type
:
Number
,
required
:
true
,
},
milestonesExpanded
:
{
type
:
Boolean
,
required
:
true
,
},
},
};
</
script
>
...
...
@@ -33,19 +37,22 @@ export default {
<span
v-for=
"timeframeItem in timeframe"
:key=
"timeframeItem.id"
class=
"milestone-timeline-cell d-table-cell position-relative border-right border-bottom"
class=
"milestone-timeline-cell gl-display-table-cell gl-relative border-right border-bottom"
:class=
"
{ 'milestone-timeline-cell-empty': !milestonesExpanded }"
data-qa-selector="milestone_timeline_cell"
>
<current-day-indicator
:preset-type=
"presetType"
:timeframe-item=
"timeframeItem"
/>
<milestone-item
v-for=
"milestone in milestones"
:key=
"milestone.id"
:preset-type=
"presetType"
:milestone=
"milestone"
:timeframe=
"timeframe"
:timeframe-item=
"timeframeItem"
:current-group-id=
"currentGroupId"
/>
<template
v-if=
"milestonesExpanded"
>
<milestone-item
v-for=
"milestone in milestones"
:key=
"milestone.id"
:preset-type=
"presetType"
:milestone=
"milestone"
:timeframe=
"timeframe"
:timeframe-item=
"timeframeItem"
:current-group-id=
"currentGroupId"
/>
</
template
>
</span>
</div>
</template>
ee/app/assets/javascripts/roadmap/components/milestones_list_section.vue
View file @
d1eec7ce
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
eventHub
from
'
../event_hub
'
;
import
{
__
,
n__
}
from
'
~/locale
'
;
import
{
GlButton
,
GlIcon
,
GlTooltipDirective
}
from
'
@gitlab/ui
'
;
import
{
EPIC_DETAILS_CELL_WIDTH
,
EPIC_ITEM_HEIGHT
,
TIMELINE_CELL_MIN_WIDTH
}
from
'
../constants
'
;
import
MilestoneTimeline
from
'
./milestone_timeline.vue
'
;
const
EXPAND_BUTTON_EXPANDED
=
{
name
:
'
chevron-down
'
,
iconLabel
:
__
(
'
Collapse milestones
'
),
tooltip
:
__
(
'
Collapse
'
),
};
const
EXPAND_BUTTON_COLLAPSED
=
{
name
:
'
chevron-right
'
,
iconLabel
:
__
(
'
Expand milestones
'
),
tooltip
:
__
(
'
Expand
'
),
};
export
default
{
components
:
{
MilestoneTimeline
,
GlButton
,
GlIcon
,
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
props
:
{
presetType
:
{
...
...
@@ -31,6 +50,7 @@ export default {
offsetLeft
:
0
,
showBottomShadow
:
false
,
roadmapShellEl
:
null
,
milestonesExpanded
:
true
,
};
},
computed
:
{
...
...
@@ -48,6 +68,17 @@ export default {
left
:
`
${
this
.
offsetLeft
}
px`
,
};
},
expandButton
()
{
return
this
.
milestonesExpanded
?
EXPAND_BUTTON_EXPANDED
:
EXPAND_BUTTON_COLLAPSED
;
},
milestonesCount
()
{
return
this
.
milestones
.
length
;
},
milestonesCountText
()
{
return
Number
.
isInteger
(
this
.
milestonesCount
)
?
n__
(
`%d milestone`
,
`%d milestones`
,
this
.
milestonesCount
)
:
''
;
},
},
mounted
()
{
eventHub
.
$on
(
'
epicsListScrolled
'
,
this
.
handleEpicsListScroll
);
...
...
@@ -76,23 +107,59 @@ export default {
handleEpicsListScroll
({
scrollTop
,
clientHeight
,
scrollHeight
})
{
this
.
showBottomShadow
=
Math
.
ceil
(
scrollTop
)
+
clientHeight
<
scrollHeight
;
},
toggleMilestonesExpanded
()
{
this
.
milestonesExpanded
=
!
this
.
milestonesExpanded
;
},
},
};
</
script
>
<
template
>
<div
:style=
"sectionContainerStyles"
class=
"milestones-list-section d-table"
>
<div
:style=
"sectionContainerStyles"
class=
"milestones-list-section gl-display-table"
:class=
"
{ 'milestones-list-section-collapsed': !milestonesExpanded }"
>
<div
class=
"milestones-list-title
d-table-cell bold border-bottom align-top position-sticky pt-2 pl
-3"
class=
"milestones-list-title
gl-display-table-cell border-bottom gl-vertical-align-top position-sticky gl-p
-3"
>
{{
__
(
'
Milestones
'
)
}}
<div
class=
"gl-display-flex gl-align-items-center"
>
<span
v-gl-tooltip.hover.topright=
"
{
title: expandButton.tooltip,
offset: 15,
boundary: 'viewport',
}"
data-testid="expandButton"
>
<gl-button
:aria-label=
"expandButton.iconLabel"
variant=
"link"
@
click=
"toggleMilestonesExpanded"
>
<gl-icon
:name=
"expandButton.name"
class=
"text-secondary"
aria-hidden=
"true"
/>
</gl-button>
</span>
<div
class=
"gl-overflow-hidden gl-flex-grow-1 gl-mx-3 gl-font-weight-bold"
>
{{
__
(
'
Milestones
'
)
}}
</div>
<div
v-gl-tooltip=
"milestonesCountText"
class=
"gl-display-flex gl-align-items-center gl-justify-content-center text-secondary gl-white-space-nowrap"
data-testid=
"count"
>
<gl-icon
name=
"clock"
class=
"gl-mr-2"
aria-hidden=
"true"
/>
<span
:aria-label=
"milestonesCountText"
>
{{
milestonesCount
}}
</span>
</div>
</div>
</div>
<div
class=
"milestones-list-items
d
-table-cell"
>
<div
class=
"milestones-list-items
gl-display
-table-cell"
>
<milestone-timeline
:preset-type=
"presetType"
:timeframe=
"timeframe"
:milestones=
"milestones"
:current-group-id=
"currentGroupId"
:milestones-expanded=
"milestonesExpanded"
/>
</div>
<div
v-show=
"showBottomShadow"
:style=
"shadowCellStyles"
class=
"scroll-bottom-shadow"
></div>
...
...
ee/app/assets/stylesheets/pages/roadmap.scss
View file @
d1eec7ce
$header-item-height
:
60px
;
$item-height
:
50px
;
$milestones-collapsed-height
:
38px
;
$details-cell-width
:
320px
;
$timeline-cell-width
:
180px
;
$border-style
:
1px
solid
$border-gray-normal
;
...
...
@@ -408,11 +409,19 @@ html.group-epics-roadmap-html {
}
.milestones-list-section
{
&
.milestones-list-section-collapsed
{
height
:
$milestones-collapsed-height
;
}
.milestones-list-items
{
.milestone-timeline-cell
{
width
:
$timeline-cell-width
;
}
.milestone-timeline-cell-empty
{
height
:
$milestones-collapsed-height
;
}
.timeline-bar-wrapper
{
height
:
32px
;
color
:
$gray-700
;
...
...
ee/changelogs/unreleased/212494-allow-hiding-collapsing-of-milestone-header-on-roadmap.yml
0 → 100644
View file @
d1eec7ce
---
title
:
Allow Hiding/Collapsing of Milestone header on Roadmap
merge_request
:
34357
author
:
type
:
added
ee/spec/frontend/roadmap/components/epic_item_details_spec.js
View file @
d1eec7ce
import
{
GlButton
,
GlIcon
,
GlTooltip
}
from
'
@gitlab/ui
'
;
import
{
GlButton
,
GlIcon
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
createStore
from
'
ee/roadmap/store
'
;
import
EpicItemDetails
from
'
ee/roadmap/components/epic_item_details.vue
'
;
...
...
@@ -10,45 +10,12 @@ import {
mockFormattedChildEpic1
,
}
from
'
ee_jest/roadmap/mock_data
'
;
let
store
;
const
createComponent
=
({
epic
=
mockFormattedEpic
,
currentGroupId
=
mockGroupId
,
timeframeString
=
'
Jul 10, 2017 – Jun 2, 2018
'
,
childLevel
=
0
,
childrenFlags
=
{
'
41
'
:
{
itemExpanded
:
false
}
},
hasFiltersApplied
=
false
,
isChildrenEmpty
=
false
,
}
=
{})
=>
{
return
shallowMount
(
EpicItemDetails
,
{
store
,
propsData
:
{
epic
,
currentGroupId
,
timeframeString
,
childLevel
,
childrenFlags
,
hasFiltersApplied
,
isChildrenEmpty
,
},
});
};
const
getTitle
=
wrapper
=>
wrapper
.
find
(
'
.epic-title
'
);
const
getGroupName
=
wrapper
=>
wrapper
.
find
(
'
.epic-group
'
);
const
getExpandIconButton
=
wrapper
=>
wrapper
.
find
(
GlButton
);
const
getChildEpicsCount
=
wrapper
=>
wrapper
.
find
({
ref
:
'
childEpicsCount
'
});
describe
(
'
EpicItemDetails
'
,
()
=>
{
let
wrapper
;
let
store
;
beforeEach
(()
=>
{
store
=
createStore
();
wrapper
=
createComponent
();
});
afterEach
(()
=>
{
...
...
@@ -56,67 +23,104 @@ describe('EpicItemDetails', () => {
wrapper
=
null
;
});
describe
(
'
epic title
'
,
()
=>
{
it
(
'
is displayed
'
,
()
=>
{
expect
(
getTitle
(
wrapper
).
text
()).
toBe
(
mockFormattedEpic
.
title
);
const
createWrapper
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
EpicItemDetails
,
{
store
,
propsData
:
{
epic
:
mockFormattedEpic
,
currentGroupId
:
mockGroupId
,
timeframeString
:
'
Jul 10, 2017 – Jun 2, 2018
'
,
childLevel
:
0
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
false
}
},
hasFiltersApplied
:
false
,
isChildrenEmpty
:
false
,
...
props
,
},
});
};
it
(
'
contains a link to the epic
'
,
()
=>
{
expect
(
getTitle
(
wrapper
).
attributes
(
'
href
'
)).
toBe
(
mockFormattedEpic
.
webUrl
);
});
const
getTitle
=
()
=>
wrapper
.
find
(
'
.epic-title
'
);
const
getGroupName
=
()
=>
wrapper
.
find
(
'
.epic-group
'
);
const
getChildMarginClassName
=
()
=>
wrapper
.
vm
.
childMarginClassname
;
const
getExpandIconButton
=
()
=>
wrapper
.
find
(
GlButton
);
const
getExpandIconTooltip
=
()
=>
wrapper
.
find
({
ref
:
'
expandIconTooltip
'
});
const
getChildEpicsCount
=
()
=>
wrapper
.
find
({
ref
:
'
childEpicsCount
'
});
const
getChildEpicsCountTooltip
=
()
=>
wrapper
.
find
({
ref
:
'
childEpicsCountTooltip
'
});
const
getExpandButtonData
=
()
=>
({
icon
:
wrapper
.
find
(
GlIcon
).
attributes
(
'
name
'
),
iconLabel
:
getExpandIconButton
().
attributes
(
'
aria-label
'
),
tooltip
:
getExpandIconTooltip
().
text
(),
});
describe
(
'
epic group name
'
,
()
=>
{
describe
(
'
when the epic group ID is different from the current group ID
'
,
()
=>
{
let
epic
;
beforeEach
(()
=>
{
epic
=
{
id
:
'
41
'
,
mockFormattedEpic
,
groupId
:
1
,
groupName
:
'
Bar
'
,
groupFullName
:
'
Foo / Bar
'
,
descendantCounts
:
{
closedIssues
:
3
,
openedIssues
:
2
,
},
};
wrapper
.
setProps
({
epic
,
currentGroupId
:
2
});
});
const
getEpicTitleData
=
()
=>
({
title
:
getTitle
().
text
(),
link
:
getTitle
().
attributes
(
'
href
'
),
});
it
(
'
is displayed
'
,
()
=>
{
expect
(
getGroupName
(
wrapper
).
text
()).
toContain
(
epic
.
groupName
);
});
const
getEpicGroupNameData
=
()
=>
({
groupName
:
getGroupName
().
text
(),
title
:
getGroupName
().
attributes
(
'
title
'
),
});
it
(
'
is set to the title attribute
'
,
()
=>
{
expect
(
getGroupName
(
wrapper
).
attributes
(
'
title
'
)).
toBe
(
epic
.
groupFullName
);
});
const
createMockEpic
=
epic
=>
({
...
mockFormattedEpic
,
...
epic
,
});
describe
(
'
epic title
'
,
()
=>
{
beforeEach
(()
=>
{
createWrapper
();
});
describe
(
'
when the epic group ID is the same as the current group ID
'
,
()
=>
{
let
epic
;
it
(
'
is displayed with a link to the epic
'
,
()
=>
{
expect
(
getEpicTitleData
()).
toEqual
({
title
:
mockFormattedEpic
.
title
,
link
:
mockFormattedEpic
.
webUrl
,
});
});
});
beforeEach
(()
=>
{
epic
=
{
...
mockFormattedEpic
,
groupId
:
1
,
groupName
:
'
Bar
'
,
groupFullName
:
'
Foo / Bar
'
,
};
describe
(
'
epic group name
'
,
()
=>
{
const
epic
=
{
id
:
'
41
'
,
...
mockFormattedEpic
,
groupId
:
1
,
groupName
:
'
Bar
'
,
groupFullName
:
'
Foo / Bar
'
,
descendantCounts
:
{
closedIssues
:
3
,
openedIssues
:
2
,
},
};
wrapper
.
setProps
({
epic
,
currentGroupId
:
1
});
describe
(
'
when the epic group ID is different from the current group ID
'
,
()
=>
{
it
(
'
is displayed and set to the title attribute
'
,
()
=>
{
createWrapper
({
epic
,
currentGroupId
:
2
});
expect
(
getEpicGroupNameData
()).
toEqual
({
groupName
:
epic
.
groupName
,
title
:
epic
.
groupFullName
,
});
});
});
describe
(
'
when the epic group ID is the same as the current group ID
'
,
()
=>
{
it
(
'
is hidden
'
,
()
=>
{
expect
(
getGroupName
(
wrapper
).
exists
()).
toBe
(
false
);
createWrapper
({
epic
,
currentGroupId
:
1
});
expect
(
getGroupName
().
exists
()).
toBe
(
false
);
});
});
});
describe
(
'
timeframe
'
,
()
=>
{
it
(
'
is displayed
'
,
()
=>
{
createWrapper
();
const
timeframe
=
wrapper
.
find
(
'
.epic-timeframe
'
);
expect
(
timeframe
.
text
()).
toBe
(
'
Jul 10, 2017 – Jun 2, 2018
'
);
...
...
@@ -125,13 +129,13 @@ describe('EpicItemDetails', () => {
describe
(
'
childMarginClassname
'
,
()
=>
{
it
(
'
childMarginClassname returns class for level 1 child is childLevel is 1
'
,
()
=>
{
wrapper
.
setProps
({
childLevel
:
1
});
expect
(
wrapper
.
vm
.
childMarginClassname
).
toEqual
(
'
ml-4
'
);
createWrapper
({
childLevel
:
1
});
expect
(
getChildMarginClassName
()
).
toEqual
(
'
ml-4
'
);
});
it
(
'
childMarginClassname returns class for level 2 child is childLevel is 2
'
,
()
=>
{
wrapper
.
setProps
({
childLevel
:
2
});
expect
(
wrapper
.
vm
.
childMarginClassname
).
toEqual
(
'
ml-6
'
);
createWrapper
({
childLevel
:
2
});
expect
(
getChildMarginClassName
()
).
toEqual
(
'
ml-6
'
);
});
});
...
...
@@ -141,204 +145,152 @@ describe('EpicItemDetails', () => {
});
describe
(
'
expand icon
'
,
()
=>
{
it
(
'
is hidden when epic has no child epics
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
false
,
};
wrapper
=
createComponent
({
epic
});
expect
(
getExpandIconButton
(
wrapper
).
classes
()).
toContain
(
'
invisible
'
);
});
it
(
'
is shown when epic has child epics
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
true
,
children
:
{
edges
:
[
mockFormattedChildEpic1
],
},
};
wrapper
=
createComponent
({
epic
});
expect
(
getExpandIconButton
(
wrapper
).
classes
()).
not
.
toContain
(
'
invisible
'
);
});
it
(
'
shows "chevron-right" icon when child epics are not expanded
'
,
()
=>
{
wrapper
=
createComponent
();
expect
(
wrapper
.
find
(
GlIcon
).
attributes
(
'
name
'
)).
toBe
(
'
chevron-right
'
);
});
it
(
'
shows "chevron-down" icon when child epics are expanded
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
true
,
};
wrapper
=
createComponent
({
epic
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
true
},
},
it
(
'
is hidden when it is child epic
'
,
()
=>
{
const
epic
=
createMockEpic
({
isChildEpic
:
true
,
});
expect
(
wrapper
.
find
(
GlIcon
).
attributes
(
'
name
'
)).
toBe
(
'
chevron-down
'
);
createWrapper
({
epic
});
expect
(
getExpandIconButton
().
classes
()).
toContain
(
'
invisible
'
);
});
it
(
'
shows "information-o" icon when child epics are expanded but no children are returned due to applied filters
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
true
,
};
wrapper
=
createComponent
({
epic
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
true
},
},
hasFiltersApplied
:
true
,
isChildrenEmpty
:
true
,
describe
(
'
when epic has no child epics
'
,
()
=>
{
beforeEach
(()
=>
{
const
epic
=
createMockEpic
({
hasChildren
:
false
,
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
0
,
},
});
createWrapper
({
epic
});
});
it
(
'
is hidden
'
,
()
=>
{
expect
(
getExpandIconButton
().
classes
()).
toContain
(
'
invisible
'
);
});
describe
(
'
child epics count
'
,
()
=>
{
it
(
'
shows the count as 0
'
,
()
=>
{
expect
(
getChildEpicsCount
().
text
()).
toBe
(
'
0
'
);
});
});
expect
(
wrapper
.
find
(
GlIcon
).
attributes
(
'
name
'
)).
toBe
(
'
information-o
'
);
});
it
(
'
has "Expand child epics" label when child epics are not expanded
'
,
()
=>
{
wrapper
=
createComponent
();
expect
(
getExpandIconButton
(
wrapper
).
attributes
(
'
aria-label
'
)).
toBe
(
'
Expand child epics
'
);
});
describe
(
'
when epic has child epics
'
,
()
=>
{
let
epic
;
beforeEach
(()
=>
{
epic
=
createMockEpic
({
id
:
41
,
hasChildren
:
true
,
children
:
{
edges
:
[
mockFormattedChildEpic1
],
},
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
1
,
},
});
createWrapper
({
epic
});
});
it
(
'
has "Collapse child epics" label when child epics are expanded
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
true
,
};
wrapper
=
createComponent
({
epic
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
true
},
},
it
(
'
is shown
'
,
()
=>
{
expect
(
getExpandIconButton
().
classes
()).
not
.
toContain
(
'
invisible
'
);
});
expect
(
getExpandIconButton
(
wrapper
).
attributes
(
'
aria-label
'
)).
toBe
(
'
Collapse child epics
'
);
});
it
(
'
emits toggleIsEpicExpanded event when clicked
'
,
()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
(()
=>
{});
getExpandIconButton
().
vm
.
$emit
(
'
click
'
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
toggleIsEpicExpanded
'
,
epic
);
});
it
(
'
has "No child epics match applied filters" label when child epics are expanded
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
true
,
};
wrapper
=
createComponent
({
epic
,
childrenFlags
:
{
describe
(
'
when child epics are expanded
'
,
()
=>
{
const
childrenFlags
=
{
'
41
'
:
{
itemExpanded
:
true
},
},
hasFiltersApplied
:
true
,
isChildrenEmpty
:
true
,
};
beforeEach
(()
=>
{
createWrapper
({
epic
,
childrenFlags
});
});
it
(
'
shows collapse button
'
,
()
=>
{
expect
(
getExpandButtonData
()).
toEqual
({
icon
:
'
chevron-down
'
,
iconLabel
:
'
Collapse
'
,
tooltip
:
'
Collapse
'
,
});
});
describe
(
'
when filters are applied
'
,
()
=>
{
beforeEach
(()
=>
{
createWrapper
({
epic
,
childrenFlags
,
hasFiltersApplied
:
true
,
isChildrenEmpty
:
true
,
});
});
it
(
'
shows child epics match filters button
'
,
()
=>
{
expect
(
getExpandButtonData
()).
toEqual
({
icon
:
'
information-o
'
,
iconLabel
:
'
No child epics match applied filters
'
,
tooltip
:
'
No child epics match applied filters
'
,
});
});
});
});
expect
(
getExpandIconButton
(
wrapper
).
attributes
(
'
aria-label
'
)).
toBe
(
'
No child epics match applied filters
'
,
);
});
it
(
'
emits toggleIsEpicExpanded event when clicked
'
,
()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
(()
=>
{});
const
id
=
41
;
const
epic
=
{
...
mockFormattedEpic
,
id
,
children
:
{
edges
:
[
mockFormattedChildEpic1
],
},
};
wrapper
=
createComponent
({
epic
});
getExpandIconButton
(
wrapper
).
vm
.
$emit
(
'
click
'
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
toggleIsEpicExpanded
'
,
epic
);
});
it
(
'
is hidden when it is child epic
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
isChildEpic
:
true
,
};
wrapper
=
createComponent
({
epic
});
expect
(
getExpandIconButton
(
wrapper
).
classes
()).
toContain
(
'
invisible
'
);
});
});
describe
(
'
child epics count
'
,
()
=>
{
it
(
'
shows the correct count of child epics
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
children
:
{
edges
:
[
mockFormattedChildEpic1
,
mockFormattedChildEpic2
],
},
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
2
,
},
};
wrapper
=
createComponent
({
epic
});
expect
(
getChildEpicsCount
(
wrapper
).
text
()).
toBe
(
'
2
'
);
});
it
(
'
shows the count as 0 when there are no child epics
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
0
,
},
};
wrapper
=
createComponent
({
epic
});
expect
(
getChildEpicsCount
(
wrapper
).
text
()).
toBe
(
'
0
'
);
});
it
(
'
has a tooltip with the count
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
children
:
{
edges
:
[
mockFormattedChildEpic1
],
},
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
1
,
},
};
wrapper
=
createComponent
({
epic
});
expect
(
wrapper
.
find
(
GlTooltip
).
text
()).
toBe
(
'
1 child epic
'
);
});
it
(
'
has a tooltip with the count and explanation if search is being performed
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
children
:
{
edges
:
[
mockFormattedChildEpic1
],
},
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
1
,
},
};
wrapper
=
createComponent
({
epic
,
hasFiltersApplied
:
true
});
expect
(
wrapper
.
find
(
GlTooltip
).
text
()).
toBe
(
'
1 child epic Some child epics may be hidden due to applied filters
'
,
);
});
describe
(
'
when child epics are not expanded
'
,
()
=>
{
beforeEach
(()
=>
{
const
childrenFlags
=
{
'
41
'
:
{
itemExpanded
:
false
},
};
createWrapper
({
epic
,
childrenFlags
,
});
});
it
(
'
shows expand button
'
,
()
=>
{
expect
(
getExpandButtonData
()).
toEqual
({
icon
:
'
chevron-right
'
,
iconLabel
:
'
Expand
'
,
tooltip
:
'
Expand
'
,
});
});
});
it
(
'
does not render if the user license does not support child epics
'
,
()
=>
{
store
.
state
.
allowSubEpics
=
false
;
wrapper
=
createComponent
();
expect
(
getChildEpicsCount
(
wrapper
).
exists
()).
toBe
(
false
);
describe
(
'
child epics count
'
,
()
=>
{
it
(
'
has a tooltip with the count
'
,
()
=>
{
createWrapper
({
epic
});
expect
(
getChildEpicsCountTooltip
().
text
()).
toBe
(
'
1 child epic
'
);
});
it
(
'
has a tooltip with the count and explanation if search is being performed
'
,
()
=>
{
createWrapper
({
epic
,
hasFiltersApplied
:
true
});
expect
(
getChildEpicsCountTooltip
().
text
()).
toBe
(
'
1 child epic Some child epics may be hidden due to applied filters
'
,
);
});
it
(
'
does not render if the user license does not support child epics
'
,
()
=>
{
store
.
state
.
allowSubEpics
=
false
;
createWrapper
({
epic
});
expect
(
getChildEpicsCount
().
exists
()).
toBe
(
false
);
});
it
(
'
shows the correct count of child epics
'
,
()
=>
{
epic
=
createMockEpic
({
children
:
{
edges
:
[
mockFormattedChildEpic1
,
mockFormattedChildEpic2
],
},
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
2
,
},
});
createWrapper
({
epic
});
expect
(
getChildEpicsCount
().
text
()).
toBe
(
'
2
'
);
});
});
});
});
});
...
...
ee/spec/frontend/roadmap/components/milestone_timeline_spec.js
View file @
d1eec7ce
import
Vue
from
'
vue
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
m
ilestoneTimelineComponent
from
'
ee/roadmap/components/milestone_timeline.vue
'
;
import
M
ilestoneTimelineComponent
from
'
ee/roadmap/components/milestone_timeline.vue
'
;
import
MilestoneItem
from
'
ee/roadmap/components/milestone_item.vue
'
;
import
{
getTimeframeForMonthsView
}
from
'
ee/roadmap/utils/roadmap_utils
'
;
...
...
@@ -11,24 +10,6 @@ import { mockTimeframeInitialDate, mockMilestone2, mockGroupId } from 'ee_jest/r
const
mockTimeframeMonths
=
getTimeframeForMonthsView
(
mockTimeframeInitialDate
);
const
createComponent
=
({
presetType
=
PRESET_TYPES
.
MONTHS
,
timeframe
=
mockTimeframeMonths
,
milestones
=
[
mockMilestone2
],
currentGroupId
=
mockGroupId
,
}
=
{})
=>
{
const
Component
=
Vue
.
extend
(
milestoneTimelineComponent
);
return
shallowMount
(
Component
,
{
propsData
:
{
presetType
,
timeframe
,
milestones
,
currentGroupId
,
},
});
};
describe
(
'
MilestoneTimelineComponent
'
,
()
=>
{
let
wrapper
;
...
...
@@ -36,17 +17,39 @@ describe('MilestoneTimelineComponent', () => {
wrapper
.
destroy
();
});
describe
(
'
template
'
,
()
=>
{
it
(
'
renders component container element with class `milestone-timeline-cell`
'
,
()
=>
{
wrapper
=
createComponent
();
expect
(
wrapper
.
find
(
'
.milestone-timeline-cell
'
).
exists
()).
toBe
(
true
);
const
createWrapper
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
MilestoneTimelineComponent
,
{
propsData
:
{
presetType
:
PRESET_TYPES
.
MONTHS
,
timeframe
:
mockTimeframeMonths
,
milestones
:
[
mockMilestone2
],
currentGroupId
:
mockGroupId
,
milestonesExpanded
:
true
,
...
props
,
},
});
};
const
findMilestoneTimelineCell
=
()
=>
wrapper
.
find
(
'
.milestone-timeline-cell
'
);
const
findMilestoneItem
=
()
=>
wrapper
.
find
(
MilestoneItem
);
describe
.
each
`
props | hasCellEmpty | hasMilestoneItem
${{}}
|
$
{
false
}
|
${
true
}
${{
milestonesExpanded
:
false
}
} |
${
true
}
|
${
false
}
`
(
'
with $props
'
,
({
props
,
hasCellEmpty
,
hasMilestoneItem
})
=>
{
beforeEach
(()
=>
{
createWrapper
(
props
);
});
it
(
'
renders MilestoneItem component
'
,
()
=>
{
wrapper
=
createComponent
();
it
(
`renders timeline cell with empty class =
${
hasCellEmpty
}
`
,
()
=>
{
expect
(
findMilestoneTimelineCell
().
classes
(
'
milestone-timeline-cell-empty
'
)).
toBe
(
hasCellEmpty
,
);
});
expect
(
wrapper
.
find
(
MilestoneItem
).
exists
()).
toBe
(
true
);
it
(
`renders MilestoneItem component =
${
hasMilestoneItem
}
`
,
()
=>
{
expect
(
findMilestoneItem
().
exists
()).
toBe
(
hasMilestoneItem
);
});
});
});
ee/spec/frontend/roadmap/components/milestones_list_section_spec.js
View file @
d1eec7ce
import
{
GlIcon
,
GlButton
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
milestonesListSectionComponent
from
'
ee/roadmap/components/milestones_list_section.vue
'
;
import
MilestoneTimeline
from
'
ee/roadmap/components/milestone_timeline.vue
'
;
...
...
@@ -9,57 +10,72 @@ import {
TIMELINE_CELL_MIN_WIDTH
,
}
from
'
ee/roadmap/constants
'
;
import
{
mockTimeframeInitialDate
,
mockGroupId
,
rawMilestones
}
from
'
ee_jest/roadmap/mock_data
'
;
const
mockTimeframeMonths
=
getTimeframeForMonthsView
(
mockTimeframeInitialDate
);
const
store
=
createStore
();
store
.
dispatch
(
'
setInitialData
'
,
{
currentGroupId
:
mockGroupId
,
presetType
:
PRESET_TYPES
.
MONTHS
,
timeframe
:
mockTimeframeMonths
,
});
store
.
dispatch
(
'
receiveMilestonesSuccess
'
,
{
rawMilestones
});
const
mockMilestones
=
store
.
state
.
milestones
;
const
createComponent
=
({
milestones
=
mockMilestones
,
timeframe
=
mockTimeframeMonths
,
currentGroupId
=
mockGroupId
,
presetType
=
PRESET_TYPES
.
MONTHS
,
}
=
{})
=>
{
const
localVue
=
createLocalVue
();
return
shallowMount
(
milestonesListSectionComponent
,
{
localVue
,
store
,
stubs
:
{
MilestoneTimeline
:
false
,
},
propsData
:
{
presetType
,
milestones
,
timeframe
,
currentGroupId
,
},
import
{
createMockDirective
,
getBinding
}
from
'
helpers/vue_mock_directive
'
;
const
initializeStore
=
mockTimeframeMonths
=>
{
const
store
=
createStore
();
store
.
dispatch
(
'
setInitialData
'
,
{
currentGroupId
:
mockGroupId
,
presetType
:
PRESET_TYPES
.
MONTHS
,
timeframe
:
mockTimeframeMonths
,
});
store
.
dispatch
(
'
receiveMilestonesSuccess
'
,
{
rawMilestones
});
return
store
;
};
describe
(
'
MilestonesListSectionComponent
'
,
()
=>
{
let
wrapper
;
let
store
;
const
mockTimeframeMonths
=
getTimeframeForMonthsView
(
mockTimeframeInitialDate
);
const
findMilestoneCount
=
()
=>
wrapper
.
find
(
'
[data-testid="count"]
'
);
const
findMilestoneCountTooltip
=
()
=>
getBinding
(
findMilestoneCount
().
element
,
'
gl-tooltip
'
);
const
findExpandButtonContainer
=
()
=>
wrapper
.
find
(
'
[data-testid="expandButton"]
'
);
const
findExpandButtonData
=
()
=>
{
const
container
=
findExpandButtonContainer
();
return
{
icon
:
container
.
find
(
GlIcon
).
attributes
(
'
name
'
),
iconLabel
:
container
.
find
(
GlButton
).
attributes
(
'
aria-label
'
),
tooltip
:
getBinding
(
container
.
element
,
'
gl-tooltip
'
).
value
.
title
,
};
};
const
createWrapper
=
(
props
=
{})
=>
{
const
localVue
=
createLocalVue
();
wrapper
=
shallowMount
(
milestonesListSectionComponent
,
{
localVue
,
store
,
stubs
:
{
MilestoneTimeline
:
false
,
},
propsData
:
{
milestones
:
store
.
state
.
milestones
,
timeframe
:
mockTimeframeMonths
,
currentGroupId
:
mockGroupId
,
presetType
:
PRESET_TYPES
.
MONTHS
,
...
props
,
},
directives
:
{
GlTooltip
:
createMockDirective
(),
},
});
};
beforeEach
(()
=>
{
wrapper
=
createComponent
();
store
=
initializeStore
(
mockTimeframeMonths
);
createWrapper
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
describe
(
'
data
'
,
()
=>
{
it
(
'
returns default data props
'
,
()
=>
{
expect
(
wrapper
.
vm
.
offsetLeft
).
toBe
(
0
);
expect
(
wrapper
.
vm
.
roadmapShellEl
).
toBeDefined
();
expect
(
wrapper
.
vm
.
milestonesExpanded
).
toBe
(
true
);
});
});
...
...
@@ -134,5 +150,41 @@ describe('MilestonesListSectionComponent', () => {
expect
(
wrapper
.
find
(
'
.scroll-bottom-shadow
'
).
exists
()).
toBe
(
true
);
});
it
(
'
show the correct count of milestones
'
,
()
=>
{
expect
(
findMilestoneCount
().
text
()).
toBe
(
'
2
'
);
});
it
(
'
has a tooltip with the correct count of milestones
'
,
()
=>
{
expect
(
findMilestoneCountTooltip
().
value
).
toBe
(
'
2 milestones
'
);
});
describe
(
'
milestone expand/collapse button
'
,
()
=>
{
it
(
'
is rendered
'
,
()
=>
{
expect
(
findExpandButtonData
()).
toEqual
({
icon
:
'
chevron-down
'
,
iconLabel
:
'
Collapse milestones
'
,
tooltip
:
'
Collapse
'
,
});
});
});
});
describe
(
'
when the milestone list is expanded
'
,
()
=>
{
beforeEach
(()
=>
{
findExpandButtonContainer
()
.
find
(
GlButton
)
.
vm
.
$emit
(
'
click
'
);
return
wrapper
.
vm
.
$nextTick
();
});
it
(
'
shows "chevron-right" icon when the milestone toggle button is clicked
'
,
()
=>
{
expect
(
findExpandButtonData
()).
toEqual
({
icon
:
'
chevron-right
'
,
iconLabel
:
'
Expand milestones
'
,
tooltip
:
'
Expand
'
,
});
});
});
});
locale/gitlab.pot
View file @
d1eec7ce
...
...
@@ -199,6 +199,11 @@ msgid_plural "%d metrics"
msgstr[0] ""
msgstr[1] ""
msgid "%d milestone"
msgid_plural "%d milestones"
msgstr[0] ""
msgstr[1] ""
msgid "%d minute"
msgid_plural "%d minutes"
msgstr[0] ""
...
...
@@ -5689,7 +5694,7 @@ msgstr ""
msgid "Collapse approvers"
msgstr ""
msgid "Collapse
child epic
s"
msgid "Collapse
milestone
s"
msgstr ""
msgid "Collapse replies"
...
...
@@ -9296,15 +9301,15 @@ msgstr ""
msgid "Expand approvers"
msgstr ""
msgid "Expand child epics"
msgstr ""
msgid "Expand down"
msgstr ""
msgid "Expand dropdown"
msgstr ""
msgid "Expand milestones"
msgstr ""
msgid "Expand sidebar"
msgstr ""
...
...
spec/frontend/helpers/vue_mock_directive.js
View file @
d1eec7ce
...
...
@@ -2,13 +2,21 @@ export const getKey = name => `$_gl_jest_${name}`;
export
const
getBinding
=
(
el
,
name
)
=>
el
[
getKey
(
name
)];
const
writeBindingToElement
=
(
el
,
{
name
,
value
,
arg
,
modifiers
})
=>
{
el
[
getKey
(
name
)]
=
{
value
,
arg
,
modifiers
,
};
};
export
const
createMockDirective
=
()
=>
({
bind
(
el
,
{
name
,
value
,
arg
,
modifiers
}
)
{
el
[
getKey
(
name
)]
=
{
value
,
arg
,
modifiers
,
}
;
bind
(
el
,
binding
)
{
writeBindingToElement
(
el
,
binding
);
}
,
update
(
el
,
binding
)
{
writeBindingToElement
(
el
,
binding
)
;
},
unbind
(
el
,
{
name
})
{
...
...
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