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
Show 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=
"d-flex align-items-start p-2"
class=
"epic-details-cell gl-display-flex gl-flex-direction-column gl-justify-content-center"
data-qa-selector=
"epic_details_cell"
>
<div
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,10 +37,12 @@ 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"
/>
<template
v-if=
"milestonesExpanded"
>
<milestone-item
v-for=
"milestone in milestones"
:key=
"milestone.id"
...
...
@@ -46,6 +52,7 @@ export default {
: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
class=
"milestones-list-title d-table-cell bold border-bottom align-top position-sticky pt-2 pl-3"
:style=
"sectionContainerStyles"
class=
"milestones-list-section gl-display-table"
:class=
"
{ 'milestones-list-section-collapsed': !milestonesExpanded }"
>
<div
class=
"milestones-list-title gl-display-table-cell border-bottom gl-vertical-align-top position-sticky gl-p-3"
>
<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
class=
"milestones-list-items d-table-cell"
>
<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 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,70 +10,87 @@ 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
,
{
describe
(
'
EpicItemDetails
'
,
()
=>
{
let
wrapper
;
let
store
;
beforeEach
(()
=>
{
store
=
createStore
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
});
const
createWrapper
=
(
props
=
{})
=>
{
wrapper
=
shallowMount
(
EpicItemDetails
,
{
store
,
propsData
:
{
epic
,
currentGroupId
,
timeframeString
,
childLevel
,
childrenFlags
,
hasFiltersApplied
,
isChildrenEmpty
,
epic
:
mockFormattedEpic
,
currentGroupId
:
mockGroupId
,
timeframeString
:
'
Jul 10, 2017 – Jun 2, 2018
'
,
childLevel
:
0
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
false
}
},
hasFiltersApplied
:
false
,
isChildrenEmpty
:
false
,
...
props
,
},
});
};
};
const
getTitle
=
wrapper
=>
wrapper
.
find
(
'
.epic-title
'
);
const
getTitle
=
()
=>
wrapper
.
find
(
'
.epic-title
'
);
const
getGroupName
=
wrapper
=>
wrapper
.
find
(
'
.epic-group
'
);
const
getGroupName
=
()
=>
wrapper
.
find
(
'
.epic-group
'
);
const
getExpandIconButton
=
wrapper
=>
wrapper
.
find
(
GlButton
)
;
const
getChildMarginClassName
=
()
=>
wrapper
.
vm
.
childMarginClassname
;
const
getChildEpicsCount
=
wrapper
=>
wrapper
.
find
({
ref
:
'
childEpicsCount
'
}
);
const
getExpandIconButton
=
()
=>
wrapper
.
find
(
GlButton
);
describe
(
'
EpicItemDetails
'
,
()
=>
{
let
wrapper
;
const
getExpandIconTooltip
=
()
=>
wrapper
.
find
({
ref
:
'
expandIconTooltip
'
});
beforeEach
(()
=>
{
store
=
createStore
();
wrapper
=
createComponent
();
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
(),
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
const
getEpicTitleData
=
()
=>
({
title
:
getTitle
().
text
(),
link
:
getTitle
().
attributes
(
'
href
'
),
});
const
getEpicGroupNameData
=
()
=>
({
groupName
:
getGroupName
().
text
(),
title
:
getGroupName
().
attributes
(
'
title
'
),
});
const
createMockEpic
=
epic
=>
({
...
mockFormattedEpic
,
...
epic
,
});
describe
(
'
epic title
'
,
()
=>
{
it
(
'
is displayed
'
,
()
=>
{
expect
(
getTitle
(
wrapper
).
text
()).
toBe
(
mockFormattedEpic
.
title
);
beforeEach
(
()
=>
{
createWrapper
(
);
});
it
(
'
contains a link to the epic
'
,
()
=>
{
expect
(
getTitle
(
wrapper
).
attributes
(
'
href
'
)).
toBe
(
mockFormattedEpic
.
webUrl
);
it
(
'
is displayed with a link to the epic
'
,
()
=>
{
expect
(
getEpicTitleData
()).
toEqual
({
title
:
mockFormattedEpic
.
title
,
link
:
mockFormattedEpic
.
webUrl
,
});
});
});
describe
(
'
epic group name
'
,
()
=>
{
describe
(
'
when the epic group ID is different from the current group ID
'
,
()
=>
{
let
epic
;
beforeEach
(()
=>
{
epic
=
{
const
epic
=
{
id
:
'
41
'
,
mockFormattedEpic
,
...
mockFormattedEpic
,
groupId
:
1
,
groupName
:
'
Bar
'
,
groupFullName
:
'
Foo / Bar
'
,
...
...
@@ -83,40 +100,27 @@ describe('EpicItemDetails', () => {
},
};
wrapper
.
setProps
({
epic
,
currentGroupId
:
2
});
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
,
});
it
(
'
is displayed
'
,
()
=>
{
expect
(
getGroupName
(
wrapper
).
text
()).
toContain
(
epic
.
groupName
);
});
it
(
'
is set to the title attribute
'
,
()
=>
{
expect
(
getGroupName
(
wrapper
).
attributes
(
'
title
'
)).
toBe
(
epic
.
groupFullName
);
});
});
describe
(
'
when the epic group ID is the same as the current group ID
'
,
()
=>
{
let
epic
;
beforeEach
(()
=>
{
epic
=
{
...
mockFormattedEpic
,
groupId
:
1
,
groupName
:
'
Bar
'
,
groupFullName
:
'
Foo / Bar
'
,
};
wrapper
.
setProps
({
epic
,
currentGroupId
:
1
});
});
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
});
it
(
'
is hidden when it is child epic
'
,
()
=>
{
const
epic
=
createMockEpic
({
isChildEpic
:
true
,
});
createWrapper
({
epic
});
expect
(
getExpandIconButton
().
classes
()).
toContain
(
'
invisible
'
);
});
expect
(
getExpandIconButton
(
wrapper
).
classes
()).
toContain
(
'
invisible
'
);
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
'
);
});
});
});
it
(
'
is shown when epic has child epics
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
describe
(
'
when epic has child epics
'
,
()
=>
{
let
epic
;
beforeEach
(()
=>
{
epic
=
createMockEpic
({
id
:
41
,
hasChildren
:
true
,
children
:
{
edges
:
[
mockFormattedChildEpic1
],
},
};
wrapper
=
createComponent
({
epic
});
expect
(
getExpandIconButton
(
wrapper
).
classes
()).
not
.
toContain
(
'
invisible
'
);
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
1
,
},
});
createWrapper
({
epic
});
});
it
(
'
shows "chevron-right" icon when child epics are not expanded
'
,
()
=>
{
wrapper
=
createComponent
();
it
(
'
is shown
'
,
()
=>
{
expect
(
getExpandIconButton
().
classes
()).
not
.
toContain
(
'
invisible
'
);
});
expect
(
wrapper
.
find
(
GlIcon
).
attributes
(
'
name
'
)).
toBe
(
'
chevron-right
'
);
it
(
'
emits toggleIsEpicExpanded event when clicked
'
,
()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
(()
=>
{});
getExpandIconButton
().
vm
.
$emit
(
'
click
'
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
toggleIsEpicExpanded
'
,
epic
);
});
it
(
'
shows "chevron-down" icon 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
},
},
};
beforeEach
(()
=>
{
createWrapper
({
epic
,
childrenFlags
});
});
expect
(
wrapper
.
find
(
GlIcon
).
attributes
(
'
name
'
)).
toBe
(
'
chevron-down
'
);
it
(
'
shows collapse button
'
,
()
=>
{
expect
(
getExpandButtonData
()).
toEqual
({
icon
:
'
chevron-down
'
,
iconLabel
:
'
Collapse
'
,
tooltip
:
'
Collapse
'
,
});
});
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
({
describe
(
'
when filters are applied
'
,
()
=>
{
beforeEach
(()
=>
{
createWrapper
({
epic
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
true
},
},
childrenFlags
,
hasFiltersApplied
:
true
,
isChildrenEmpty
:
true
,
});
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
'
);
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
'
,
});
});
it
(
'
has "Collapse child epics" label when child epics are expanded
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
true
,
};
wrapper
=
createComponent
({
epic
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
true
},
},
});
expect
(
getExpandIconButton
(
wrapper
).
attributes
(
'
aria-label
'
)).
toBe
(
'
Collapse child epics
'
);
});
it
(
'
has "No child epics match applied filters" label when child epics are
expanded
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
hasChildren
:
true
,
describe
(
'
when child epics are not
expanded
'
,
()
=>
{
beforeEach
(()
=>
{
const
childrenFlags
=
{
'
41
'
:
{
itemExpanded
:
false
}
,
};
wrapper
=
createComponent
({
createWrapper
({
epic
,
childrenFlags
:
{
'
41
'
:
{
itemExpanded
:
true
},
},
hasFiltersApplied
:
true
,
isChildrenEmpty
:
true
,
childrenFlags
,
});
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
(
'
shows expand button
'
,
()
=>
{
expect
(
getExpandButtonData
()).
toEqual
({
icon
:
'
chevron-right
'
,
iconLabel
:
'
Expand
'
,
tooltip
:
'
Expand
'
,
});
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
(
'
has a tooltip with the count
'
,
()
=>
{
createWrapper
({
epic
});
expect
(
getChildEpicsCountTooltip
().
text
()).
toBe
(
'
1 child epic
'
);
});
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 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
(
'
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
(
'
does not render if the user license does not support child epics
'
,
()
=>
{
store
.
state
.
allowSubEpics
=
false
;
createWrapper
({
epic
});
expect
(
getChildEpicsCount
().
exists
()).
toBe
(
false
);
});
it
(
'
has a tooltip with the count and explanation if search is being performed
'
,
()
=>
{
const
epic
=
{
...
mockFormattedEpic
,
it
(
'
shows the correct count of child epics
'
,
()
=>
{
epic
=
createMockEpic
({
children
:
{
edges
:
[
mockFormattedChildEpic1
],
edges
:
[
mockFormattedChildEpic1
,
mockFormattedChildEpic2
],
},
descendantCounts
:
{
openedEpics
:
0
,
closedEpics
:
1
,
closedEpics
:
2
,
},
};
wrapper
=
createComponent
({
epic
,
hasFiltersApplied
:
true
});
expect
(
wrapper
.
find
(
GlTooltip
).
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
;
wrapper
=
createComponent
();
expect
(
getChildEpicsCount
(
wrapper
).
exists
()).
toBe
(
false
);
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
'
;
import
{
createMockDirective
,
getBinding
}
from
'
helpers/vue_mock_directive
'
;
const
mockTimeframeMonths
=
getTimeframeForMonthsView
(
mockTimeframeInitialDate
);
const
store
=
createStore
();
store
.
dispatch
(
'
setInitialData
'
,
{
const
initializeStore
=
mockTimeframeMonths
=>
{
const
store
=
createStore
();
store
.
dispatch
(
'
setInitialData
'
,
{
currentGroupId
:
mockGroupId
,
presetType
:
PRESET_TYPES
.
MONTHS
,
timeframe
:
mockTimeframeMonths
,
});
store
.
dispatch
(
'
receiveMilestonesSuccess
'
,
{
rawMilestones
});
const
mockMilestones
=
store
.
state
.
milestones
;
});
store
.
dispatch
(
'
receiveMilestonesSuccess
'
,
{
rawMilestones
});
return
store
;
};
const
createComponent
=
({
milestones
=
mockMilestones
,
timeframe
=
mockTimeframeMonths
,
currentGroupId
=
mockGroupId
,
presetType
=
PRESET_TYPES
.
MONTHS
,
}
=
{})
=>
{
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
();
return
shallowMount
(
milestonesListSectionComponent
,
{
wrapper
=
shallowMount
(
milestonesListSectionComponent
,
{
localVue
,
store
,
stubs
:
{
MilestoneTimeline
:
false
,
},
propsData
:
{
presetType
,
milestones
,
timeframe
,
currentGroupId
,
milestones
:
store
.
state
.
milestones
,
timeframe
:
mockTimeframeMonths
,
currentGroupId
:
mockGroupId
,
presetType
:
PRESET_TYPES
.
MONTHS
,
...
props
,
},
directives
:
{
GlTooltip
:
createMockDirective
(),
},
});
};
describe
(
'
MilestonesListSectionComponent
'
,
()
=>
{
let
wrapper
;
};
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
)];
export
const
createMockDirective
=
()
=>
({
bind
(
el
,
{
name
,
value
,
arg
,
modifiers
})
{
const
writeBindingToElement
=
(
el
,
{
name
,
value
,
arg
,
modifiers
})
=>
{
el
[
getKey
(
name
)]
=
{
value
,
arg
,
modifiers
,
};
};
export
const
createMockDirective
=
()
=>
({
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