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
7be12de8
Commit
7be12de8
authored
Oct 06, 2021
by
Kushal Pandya
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Remove `performance_roadmap` ff and legacy code
Changelog: changed EE: true
parent
28e47ff8
Changes
18
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
29 additions
and
537 deletions
+29
-537
app/graphql/types/base_field.rb
app/graphql/types/base_field.rb
+0
-1
ee/app/assets/javascripts/roadmap/components/epics_list_section.vue
...ets/javascripts/roadmap/components/epics_list_section.vue
+1
-1
ee/app/assets/javascripts/roadmap/components/roadmap_app.vue
ee/app/assets/javascripts/roadmap/components/roadmap_app.vue
+1
-65
ee/app/assets/javascripts/roadmap/components/roadmap_shell.vue
...p/assets/javascripts/roadmap/components/roadmap_shell.vue
+0
-44
ee/app/assets/javascripts/roadmap/constants.js
ee/app/assets/javascripts/roadmap/constants.js
+1
-1
ee/app/assets/javascripts/roadmap/store/actions.js
ee/app/assets/javascripts/roadmap/store/actions.js
+2
-72
ee/app/controllers/groups/roadmap_controller.rb
ee/app/controllers/groups/roadmap_controller.rb
+0
-1
ee/app/graphql/resolvers/concerns/sets_max_page_size.rb
ee/app/graphql/resolvers/concerns/sets_max_page_size.rb
+0
-28
ee/app/graphql/resolvers/epic_issues_resolver.rb
ee/app/graphql/resolvers/epic_issues_resolver.rb
+0
-3
ee/app/graphql/resolvers/epics_resolver.rb
ee/app/graphql/resolvers/epics_resolver.rb
+0
-3
ee/config/feature_flags/development/performance_roadmap.yml
ee/config/feature_flags/development/performance_roadmap.yml
+0
-8
ee/spec/features/groups/group_roadmap_spec.rb
ee/spec/features/groups/group_roadmap_spec.rb
+0
-1
ee/spec/frontend/roadmap/components/epics_list_section_spec.js
...ec/frontend/roadmap/components/epics_list_section_spec.js
+21
-35
ee/spec/frontend/roadmap/components/roadmap_app_spec.js
ee/spec/frontend/roadmap/components/roadmap_app_spec.js
+2
-70
ee/spec/frontend/roadmap/components/roadmap_shell_spec.js
ee/spec/frontend/roadmap/components/roadmap_shell_spec.js
+0
-6
ee/spec/frontend/roadmap/store/actions_spec.js
ee/spec/frontend/roadmap/store/actions_spec.js
+1
-101
ee/spec/requests/api/graphql/epics/epic_issues_resolver_spec.rb
...c/requests/api/graphql/epics/epic_issues_resolver_spec.rb
+0
-73
ee/spec/requests/api/graphql/epics/epic_resolver_spec.rb
ee/spec/requests/api/graphql/epics/epic_resolver_spec.rb
+0
-24
No files found.
app/graphql/types/base_field.rb
View file @
7be12de8
...
@@ -9,7 +9,6 @@ module Types
...
@@ -9,7 +9,6 @@ module Types
DEFAULT_COMPLEXITY
=
1
DEFAULT_COMPLEXITY
=
1
attr_reader
:deprecation
,
:doc_reference
attr_reader
:deprecation
,
:doc_reference
attr_writer
:max_page_size
# Can be removed with :performance_roadmap feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/337198
def
initialize
(
**
kwargs
,
&
block
)
def
initialize
(
**
kwargs
,
&
block
)
@calls_gitaly
=
!!
kwargs
.
delete
(
:calls_gitaly
)
@calls_gitaly
=
!!
kwargs
.
delete
(
:calls_gitaly
)
...
...
ee/app/assets/javascripts/roadmap/components/epics_list_section.vue
View file @
7be12de8
...
@@ -182,7 +182,7 @@ export default {
...
@@ -182,7 +182,7 @@ export default {
<current-day-indicator
:preset-type=
"presetType"
:timeframe-item=
"timeframeItem"
/>
<current-day-indicator
:preset-type=
"presetType"
:timeframe-item=
"timeframeItem"
/>
</span>
</span>
</div>
</div>
<gl-intersection-observer
v-if=
"glFeatures.performanceRoadmap"
@
appear=
"handleScrolledToEnd"
>
<gl-intersection-observer
@
appear=
"handleScrolledToEnd"
>
<div
<div
v-if=
"epicsFetchForNextPageInProgress"
v-if=
"epicsFetchForNextPageInProgress"
class=
"gl-text-center gl-py-3"
class=
"gl-text-center gl-py-3"
...
...
ee/app/assets/javascripts/roadmap/components/roadmap_app.vue
View file @
7be12de8
...
@@ -3,15 +3,12 @@ import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
...
@@ -3,15 +3,12 @@ import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import
Cookies
from
'
js-cookie
'
;
import
Cookies
from
'
js-cookie
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
import
{
EXTEND_AS
,
EPICS_LIMIT_DISMISSED_COOKIE_NAME
,
EPICS_LIMIT_DISMISSED_COOKIE_NAME
,
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT
,
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT
,
DATE_RANGES
,
DATE_RANGES
,
}
from
'
../constants
'
;
}
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
EpicsListEmpty
from
'
./epics_list_empty.vue
'
;
import
EpicsListEmpty
from
'
./epics_list_empty.vue
'
;
import
RoadmapFilters
from
'
./roadmap_filters.vue
'
;
import
RoadmapFilters
from
'
./roadmap_filters.vue
'
;
import
RoadmapShell
from
'
./roadmap_shell.vue
'
;
import
RoadmapShell
from
'
./roadmap_shell.vue
'
;
...
@@ -31,7 +28,6 @@ export default {
...
@@ -31,7 +28,6 @@ export default {
RoadmapFilters
,
RoadmapFilters
,
RoadmapShell
,
RoadmapShell
,
},
},
mixins
:
[
glFeatureFlagsMixin
()],
props
:
{
props
:
{
timeframeRangeType
:
{
timeframeRangeType
:
{
type
:
String
,
type
:
String
,
...
@@ -59,14 +55,11 @@ export default {
...
@@ -59,14 +55,11 @@ export default {
'
epics
'
,
'
epics
'
,
'
milestones
'
,
'
milestones
'
,
'
timeframe
'
,
'
timeframe
'
,
'
extendedTimeframe
'
,
'
epicsFetchInProgress
'
,
'
epicsFetchInProgress
'
,
'
epicsFetchForTimeframeInProgress
'
,
'
epicsFetchResultEmpty
'
,
'
epicsFetchResultEmpty
'
,
'
epicsFetchFailure
'
,
'
epicsFetchFailure
'
,
'
isChildEpics
'
,
'
isChildEpics
'
,
'
hasFiltersApplied
'
,
'
hasFiltersApplied
'
,
'
milestonesFetchFailure
'
,
'
filterParams
'
,
'
filterParams
'
,
]),
]),
showFilteredSearchbar
()
{
showFilteredSearchbar
()
{
...
@@ -91,62 +84,7 @@ export default {
...
@@ -91,62 +84,7 @@ export default {
this
.
fetchMilestones
();
this
.
fetchMilestones
();
},
},
methods
:
{
methods
:
{
...
mapActions
([
...
mapActions
([
'
fetchEpics
'
,
'
fetchMilestones
'
]),
'
fetchEpics
'
,
'
fetchEpicsForTimeframe
'
,
'
extendTimeframe
'
,
'
refreshEpicDates
'
,
'
fetchMilestones
'
,
'
refreshMilestoneDates
'
,
]),
/**
* Once timeline is expanded (either with prepend or append)
* We need performing following actions;
*
* 1. Reset start and end edges of the timeline for
* infinite scrolling to continue further.
* 2. Re-render timeline bars to account for
* updated timeframe.
* 3. In case of prepending timeframe,
* reset scroll-position (due to DOM prepend).
*/
processExtendedTimeline
({
extendAs
=
EXTEND_AS
.
PREPEND
,
roadmapTimelineEl
,
itemsCount
=
0
})
{
// Re-render timeline bars with updated timeline
eventHub
.
$emit
(
'
refreshTimeline
'
,
{
todayBarReady
:
extendAs
===
EXTEND_AS
.
PREPEND
,
});
if
(
extendAs
===
EXTEND_AS
.
PREPEND
)
{
// When DOM is prepended with elements
// we compensate the scrolling for added elements' width
roadmapTimelineEl
.
parentElement
.
scrollBy
(
roadmapTimelineEl
.
querySelector
(
'
.timeline-header-item
'
).
clientWidth
*
itemsCount
,
0
,
);
}
},
handleScrollToExtend
({
el
:
roadmapTimelineEl
,
extendAs
=
EXTEND_AS
.
PREPEND
})
{
this
.
extendTimeframe
({
extendAs
});
this
.
refreshEpicDates
();
this
.
refreshMilestoneDates
();
this
.
$nextTick
(()
=>
{
this
.
fetchEpicsForTimeframe
({
timeframe
:
this
.
extendedTimeframe
,
})
.
then
(()
=>
{
this
.
$nextTick
(()
=>
{
// Re-render timeline bars with updated timeline
this
.
processExtendedTimeline
({
itemsCount
:
this
.
extendedTimeframe
?
this
.
extendedTimeframe
.
length
:
0
,
extendAs
,
roadmapTimelineEl
,
});
});
})
.
catch
(()
=>
{});
});
},
dismissTooManyEpicsWarning
()
{
dismissTooManyEpicsWarning
()
{
Cookies
.
set
(
EPICS_LIMIT_DISMISSED_COOKIE_NAME
,
'
true
'
,
{
Cookies
.
set
(
EPICS_LIMIT_DISMISSED_COOKIE_NAME
,
'
true
'
,
{
expires
:
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT
,
expires
:
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT
,
...
@@ -193,8 +131,6 @@ export default {
...
@@ -193,8 +131,6 @@ export default {
:timeframe=
"timeframe"
:timeframe=
"timeframe"
:current-group-id=
"currentGroupId"
:current-group-id=
"currentGroupId"
:has-filters-applied=
"hasFiltersApplied"
:has-filters-applied=
"hasFiltersApplied"
@
onScrollToStart=
"handleScrollToExtend"
@
onScrollToEnd=
"handleScrollToExtend"
/>
/>
</div>
</div>
</div>
</div>
...
...
ee/app/assets/javascripts/roadmap/components/roadmap_shell.vue
View file @
7be12de8
<
script
>
<
script
>
import
{
mapState
}
from
'
vuex
'
;
import
{
mapState
}
from
'
vuex
'
;
import
{
isInViewport
}
from
'
~/lib/utils/common_utils
'
;
import
glFeatureFlagsMixin
from
'
~/vue_shared/mixins/gl_feature_flags_mixin
'
;
import
{
EXTEND_AS
}
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
epicsListSection
from
'
./epics_list_section.vue
'
;
import
epicsListSection
from
'
./epics_list_section.vue
'
;
...
@@ -16,7 +13,6 @@ export default {
...
@@ -16,7 +13,6 @@ export default {
milestonesListSection
,
milestonesListSection
,
roadmapTimelineSection
,
roadmapTimelineSection
,
},
},
mixins
:
[
glFeatureFlagsMixin
()],
props
:
{
props
:
{
presetType
:
{
presetType
:
{
type
:
String
,
type
:
String
,
...
@@ -43,56 +39,16 @@ export default {
...
@@ -43,56 +39,16 @@ export default {
required
:
true
,
required
:
true
,
},
},
},
},
data
()
{
return
{
timeframeStartOffset
:
0
,
};
},
computed
:
{
computed
:
{
...
mapState
([
'
defaultInnerHeight
'
]),
...
mapState
([
'
defaultInnerHeight
'
]),
displayMilestones
()
{
displayMilestones
()
{
return
Boolean
(
this
.
milestones
.
length
);
return
Boolean
(
this
.
milestones
.
length
);
},
},
},
},
mounted
()
{
this
.
$nextTick
(()
=>
{
// We're guarding this as in tests, `roadmapTimeline`
// is not ready when this line is executed.
if
(
this
.
$refs
.
roadmapTimeline
)
{
this
.
timeframeStartOffset
=
this
.
$refs
.
roadmapTimeline
.
$el
.
querySelector
(
'
.timeline-header-item
'
)
.
querySelector
(
'
.item-sublabel .sublabel-value:first-child
'
)
.
getBoundingClientRect
().
left
;
}
});
},
methods
:
{
methods
:
{
handleScroll
()
{
handleScroll
()
{
const
{
scrollTop
,
scrollLeft
,
clientHeight
,
scrollHeight
}
=
this
.
$el
;
const
{
scrollTop
,
scrollLeft
,
clientHeight
,
scrollHeight
}
=
this
.
$el
;
if
(
!
this
.
glFeatures
.
roadmapDaterangeFilter
)
{
const
timelineEdgeStartEl
=
this
.
$refs
.
roadmapTimeline
.
$el
.
querySelector
(
'
.timeline-header-item
'
)
.
querySelector
(
'
.item-sublabel .sublabel-value:first-child
'
);
const
timelineEdgeEndEl
=
this
.
$refs
.
roadmapTimeline
.
$el
.
querySelector
(
'
.timeline-header-item:last-child
'
)
.
querySelector
(
'
.item-sublabel .sublabel-value:last-child
'
);
// If timeline was scrolled to start
if
(
isInViewport
(
timelineEdgeStartEl
,
{
left
:
this
.
timeframeStartOffset
}))
{
this
.
$emit
(
'
onScrollToStart
'
,
{
el
:
this
.
$refs
.
roadmapTimeline
.
$el
,
extendAs
:
EXTEND_AS
.
PREPEND
,
});
}
else
if
(
isInViewport
(
timelineEdgeEndEl
))
{
// If timeline was scrolled to end
this
.
$emit
(
'
onScrollToEnd
'
,
{
el
:
this
.
$refs
.
roadmapTimeline
.
$el
,
extendAs
:
EXTEND_AS
.
APPEND
,
});
}
}
eventHub
.
$emit
(
'
epicsListScrolled
'
,
{
scrollTop
,
scrollLeft
,
clientHeight
,
scrollHeight
});
eventHub
.
$emit
(
'
epicsListScrolled
'
,
{
scrollTop
,
scrollLeft
,
clientHeight
,
scrollHeight
});
},
},
},
},
...
...
ee/app/assets/javascripts/roadmap/constants.js
View file @
7be12de8
...
@@ -81,4 +81,4 @@ export const EPICS_LIMIT_DISMISSED_COOKIE_NAME = 'epics_limit_warning_dismissed'
...
@@ -81,4 +81,4 @@ export const EPICS_LIMIT_DISMISSED_COOKIE_NAME = 'epics_limit_warning_dismissed'
export
const
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT
=
365
;
export
const
EPICS_LIMIT_DISMISSED_COOKIE_TIMEOUT
=
365
;
export
const
ROADMAP_PAGE_SIZE
=
gon
.
features
?.
performanceRoadmap
?
50
:
gon
.
roadmap_epics_limit
;
export
const
ROADMAP_PAGE_SIZE
=
50
;
ee/app/assets/javascripts/roadmap/store/actions.js
View file @
7be12de8
import
createFlash
from
'
~/flash
'
;
import
createFlash
from
'
~/flash
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
EXTEND_AS
,
ROADMAP_PAGE_SIZE
}
from
'
../constants
'
;
import
{
ROADMAP_PAGE_SIZE
}
from
'
../constants
'
;
import
epicChildEpics
from
'
../queries/epicChildEpics.query.graphql
'
;
import
epicChildEpics
from
'
../queries/epicChildEpics.query.graphql
'
;
import
groupEpics
from
'
../queries/groupEpics.query.graphql
'
;
import
groupEpics
from
'
../queries/groupEpics.query.graphql
'
;
import
groupMilestones
from
'
../queries/groupMilestones.query.graphql
'
;
import
groupMilestones
from
'
../queries/groupMilestones.query.graphql
'
;
import
*
as
epicUtils
from
'
../utils/epic_utils
'
;
import
*
as
epicUtils
from
'
../utils/epic_utils
'
;
import
*
as
roadmapItemUtils
from
'
../utils/roadmap_item_utils
'
;
import
*
as
roadmapItemUtils
from
'
../utils/roadmap_item_utils
'
;
import
{
import
{
getEpicsTimeframeRange
,
sortEpics
}
from
'
../utils/roadmap_utils
'
;
getEpicsTimeframeRange
,
sortEpics
,
extendTimeframeForPreset
,
}
from
'
../utils/roadmap_utils
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
*
as
types
from
'
./mutation_types
'
;
...
@@ -185,44 +181,6 @@ export const fetchEpics = ({ state, commit, dispatch }, { endCursor } = {}) => {
...
@@ -185,44 +181,6 @@ export const fetchEpics = ({ state, commit, dispatch }, { endCursor } = {}) => {
.
catch
(()
=>
dispatch
(
'
receiveEpicsFailure
'
));
.
catch
(()
=>
dispatch
(
'
receiveEpicsFailure
'
));
};
};
export
const
fetchEpicsForTimeframe
=
({
state
,
commit
,
dispatch
},
{
timeframe
})
=>
{
commit
(
types
.
REQUEST_EPICS_FOR_TIMEFRAME
);
return
fetchGroupEpics
(
state
,
{
timeframe
})
.
then
(({
rawEpics
,
pageInfo
})
=>
{
dispatch
(
'
receiveEpicsSuccess
'
,
{
rawEpics
,
pageInfo
,
newEpic
:
true
,
timeframeExtended
:
true
,
});
})
.
catch
(()
=>
dispatch
(
'
receiveEpicsFailure
'
));
};
/**
* Adds more EpicItemTimeline cells to the start or end of the roadmap.
*
* @param extendAs An EXTEND_AS enum value
*/
export
const
extendTimeframe
=
({
commit
,
state
},
{
extendAs
})
=>
{
const
isExtendTypePrepend
=
extendAs
===
EXTEND_AS
.
PREPEND
;
const
{
presetType
,
timeframe
}
=
state
;
const
timeframeToExtend
=
extendTimeframeForPreset
({
extendAs
,
presetType
,
initialDate
:
isExtendTypePrepend
?
roadmapItemUtils
.
timeframeStartDate
(
presetType
,
timeframe
)
:
roadmapItemUtils
.
timeframeEndDate
(
presetType
,
timeframe
),
});
if
(
isExtendTypePrepend
)
{
commit
(
types
.
PREPEND_TIMEFRAME
,
timeframeToExtend
);
}
else
{
commit
(
types
.
APPEND_TIMEFRAME
,
timeframeToExtend
);
}
};
export
const
initItemChildrenFlags
=
({
commit
},
data
)
=>
export
const
initItemChildrenFlags
=
({
commit
},
data
)
=>
commit
(
types
.
INIT_EPIC_CHILDREN_FLAGS
,
data
);
commit
(
types
.
INIT_EPIC_CHILDREN_FLAGS
,
data
);
...
@@ -256,34 +214,6 @@ export const toggleEpic = ({ state, dispatch }, { parentItem }) => {
...
@@ -256,34 +214,6 @@ export const toggleEpic = ({ state, dispatch }, { parentItem }) => {
}
}
};
};
/**
* For epics that have no start or end date, this function updates their start and end dates
* so that the epic bars get longer to appear infinitely scrolling.
*/
export
const
refreshEpicDates
=
({
commit
,
state
})
=>
{
const
{
presetType
,
timeframe
}
=
state
;
const
epics
=
state
.
epics
.
map
((
epic
)
=>
{
// Update child epic dates too
if
(
epic
.
children
?.
edges
?.
length
>
0
)
{
epic
.
children
.
edges
.
map
((
childEpic
)
=>
roadmapItemUtils
.
processRoadmapItemDates
(
childEpic
,
roadmapItemUtils
.
timeframeStartDate
(
presetType
,
timeframe
),
roadmapItemUtils
.
timeframeEndDate
(
presetType
,
timeframe
),
),
);
}
return
roadmapItemUtils
.
processRoadmapItemDates
(
epic
,
roadmapItemUtils
.
timeframeStartDate
(
presetType
,
timeframe
),
roadmapItemUtils
.
timeframeEndDate
(
presetType
,
timeframe
),
);
});
commit
(
types
.
SET_EPICS
,
epics
);
};
export
const
fetchGroupMilestones
=
(
export
const
fetchGroupMilestones
=
(
{
fullPath
,
presetType
,
filterParams
,
timeframe
},
{
fullPath
,
presetType
,
filterParams
,
timeframe
},
defaultTimeframe
,
defaultTimeframe
,
...
...
ee/app/controllers/groups/roadmap_controller.rb
View file @
7be12de8
...
@@ -8,7 +8,6 @@ module Groups
...
@@ -8,7 +8,6 @@ module Groups
before_action
:check_epics_available!
before_action
:check_epics_available!
before_action
:persist_roadmap_layout
,
only:
[
:show
]
before_action
:persist_roadmap_layout
,
only:
[
:show
]
before_action
do
before_action
do
push_frontend_feature_flag
(
:performance_roadmap
,
@group
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:roadmap_daterange_filter
,
@group
,
type: :development
,
default_enabled: :yaml
)
push_frontend_feature_flag
(
:roadmap_daterange_filter
,
@group
,
type: :development
,
default_enabled: :yaml
)
end
end
...
...
ee/app/graphql/resolvers/concerns/sets_max_page_size.rb
deleted
100644 → 0
View file @
28e47ff8
# frozen_string_literal: true
module
SetsMaxPageSize
extend
ActiveSupport
::
Concern
DEPRECATED_MAX_PAGE_SIZE
=
1000
# We no longer need 1000 page size after epics roadmap pagination feature is released,
# after :performance_roadmap flag rollout we can safely use default max page size(100)
# for epics, child epics and child issues without breaking current roadmaps.
#
# When removing :performance_roadmap flag delete this file and remove its method call and
# the fields using the resolver will keep using default max page size.
# Flag rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/337198
private
def
set_temp_limit_for
(
actor
)
max_page_size
=
if
Feature
.
enabled?
(
:performance_roadmap
,
actor
,
default_enabled: :yaml
)
context
.
schema
.
default_max_page_size
else
DEPRECATED_MAX_PAGE_SIZE
end
field
.
max_page_size
=
max_page_size
# rubocop: disable Graphql/Descriptions
end
end
ee/app/graphql/resolvers/epic_issues_resolver.rb
View file @
7be12de8
...
@@ -3,7 +3,6 @@
...
@@ -3,7 +3,6 @@
module
Resolvers
module
Resolvers
class
EpicIssuesResolver
<
BaseResolver
class
EpicIssuesResolver
<
BaseResolver
include
CachingArrayResolver
include
CachingArrayResolver
include
SetsMaxPageSize
type
Types
::
EpicIssueType
.
connection_type
,
null:
true
type
Types
::
EpicIssueType
.
connection_type
,
null:
true
...
@@ -22,8 +21,6 @@ module Resolvers
...
@@ -22,8 +21,6 @@ module Resolvers
end
end
def
query_for
(
id
)
def
query_for
(
id
)
set_temp_limit_for
(
epic
.
group
)
# Can be removed with :performance_roadmap feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/337198
::
Epic
.
related_issues
(
ids:
id
)
::
Epic
.
related_issues
(
ids:
id
)
end
end
...
...
ee/app/graphql/resolvers/epics_resolver.rb
View file @
7be12de8
...
@@ -5,7 +5,6 @@ module Resolvers
...
@@ -5,7 +5,6 @@ module Resolvers
include
TimeFrameArguments
include
TimeFrameArguments
include
SearchArguments
include
SearchArguments
include
LooksAhead
include
LooksAhead
include
SetsMaxPageSize
argument
:iid
,
GraphQL
::
Types
::
ID
,
argument
:iid
,
GraphQL
::
Types
::
ID
,
required:
false
,
required:
false
,
...
@@ -102,8 +101,6 @@ module Resolvers
...
@@ -102,8 +101,6 @@ module Resolvers
end
end
def
find_epics
(
args
)
def
find_epics
(
args
)
set_temp_limit_for
(
group
)
# Can be removed with :performance_roadmap feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/337198
apply_lookahead
(
EpicsFinder
.
new
(
context
[
:current_user
],
args
).
execute
)
apply_lookahead
(
EpicsFinder
.
new
(
context
[
:current_user
],
args
).
execute
)
end
end
...
...
ee/config/feature_flags/development/performance_roadmap.yml
deleted
100644 → 0
View file @
28e47ff8
---
name
:
performance_roadmap
introduced_by_url
:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65652
rollout_issue_url
:
https://gitlab.com/gitlab-org/gitlab/-/issues/337198
milestone
:
'
14.2'
type
:
development
group
:
group::product planning
default_enabled
:
true
ee/spec/features/groups/group_roadmap_spec.rb
View file @
7be12de8
...
@@ -29,7 +29,6 @@ RSpec.describe 'group epic roadmap', :js do
...
@@ -29,7 +29,6 @@ RSpec.describe 'group epic roadmap', :js do
before
do
before
do
stub_licensed_features
(
epics:
true
)
stub_licensed_features
(
epics:
true
)
stub_feature_flags
(
unfiltered_epic_aggregates:
false
)
stub_feature_flags
(
unfiltered_epic_aggregates:
false
)
stub_feature_flags
(
performance_roadmap:
false
)
stub_feature_flags
(
roadmap_daterange_filter:
false
)
stub_feature_flags
(
roadmap_daterange_filter:
false
)
sign_in
(
user
)
sign_in
(
user
)
...
...
ee/spec/frontend/roadmap/components/epics_list_section_spec.js
View file @
7be12de8
...
@@ -59,7 +59,6 @@ const createComponent = ({
...
@@ -59,7 +59,6 @@ const createComponent = ({
currentGroupId
=
mockGroupId
,
currentGroupId
=
mockGroupId
,
presetType
=
PRESET_TYPES
.
MONTHS
,
presetType
=
PRESET_TYPES
.
MONTHS
,
hasFiltersApplied
=
false
,
hasFiltersApplied
=
false
,
performanceRoadmap
=
false
,
}
=
{})
=>
{
}
=
{})
=>
{
return
shallowMountExtended
(
EpicsListSection
,
{
return
shallowMountExtended
(
EpicsListSection
,
{
localVue
,
localVue
,
...
@@ -75,11 +74,6 @@ const createComponent = ({
...
@@ -75,11 +74,6 @@ const createComponent = ({
currentGroupId
,
currentGroupId
,
hasFiltersApplied
,
hasFiltersApplied
,
},
},
provide
:
{
glFeatures
:
{
performanceRoadmap
,
},
},
});
});
};
};
...
@@ -87,7 +81,6 @@ describe('EpicsListSectionComponent', () => {
...
@@ -87,7 +81,6 @@ describe('EpicsListSectionComponent', () => {
let
wrapper
;
let
wrapper
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
gon
.
features
=
{
performanceRoadmap
:
false
};
wrapper
=
createComponent
();
wrapper
=
createComponent
();
});
});
...
@@ -249,6 +242,8 @@ describe('EpicsListSectionComponent', () => {
...
@@ -249,6 +242,8 @@ describe('EpicsListSectionComponent', () => {
});
});
describe
(
'
template
'
,
()
=>
{
describe
(
'
template
'
,
()
=>
{
const
findIntersectionObserver
=
()
=>
wrapper
.
findComponent
(
GlIntersectionObserver
);
it
(
'
renders component container element with class `epics-list-section`
'
,
()
=>
{
it
(
'
renders component container element with class `epics-list-section`
'
,
()
=>
{
expect
(
wrapper
.
classes
(
'
epics-list-section
'
)).
toBe
(
true
);
expect
(
wrapper
.
classes
(
'
epics-list-section
'
)).
toBe
(
true
);
});
});
...
@@ -263,22 +258,6 @@ describe('EpicsListSectionComponent', () => {
...
@@ -263,22 +258,6 @@ describe('EpicsListSectionComponent', () => {
expect
(
wrapper
.
find
(
'
.epics-list-item-empty
'
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
find
(
'
.epics-list-item-empty
'
).
exists
()).
toBe
(
true
);
});
});
it
(
'
renders bottom shadow element when `showBottomShadow` prop is true
'
,
()
=>
{
wrapper
.
setData
({
showBottomShadow
:
true
,
});
expect
(
wrapper
.
find
(
'
.epic-scroll-bottom-shadow
'
).
exists
()).
toBe
(
true
);
});
describe
(
'
when `performanceRoadmap` feature flag is enabled
'
,
()
=>
{
const
findIntersectionObserver
=
()
=>
wrapper
.
findComponent
(
GlIntersectionObserver
);
beforeEach
(()
=>
{
gon
.
features
=
{
performanceRoadmap
:
true
};
wrapper
=
createComponent
({
performanceRoadmap
:
true
});
});
it
(
'
renders gl-intersection-observer component
'
,
()
=>
{
it
(
'
renders gl-intersection-observer component
'
,
()
=>
{
expect
(
findIntersectionObserver
().
exists
()).
toBe
(
true
);
expect
(
findIntersectionObserver
().
exists
()).
toBe
(
true
);
});
});
...
@@ -301,6 +280,13 @@ describe('EpicsListSectionComponent', () => {
...
@@ -301,6 +280,13 @@ describe('EpicsListSectionComponent', () => {
expect
(
wrapper
.
findByTestId
(
'
next-page-loading
'
).
text
()).
toContain
(
'
Loading epics
'
);
expect
(
wrapper
.
findByTestId
(
'
next-page-loading
'
).
text
()).
toContain
(
'
Loading epics
'
);
expect
(
wrapper
.
findComponent
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
expect
(
wrapper
.
findComponent
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
});
});
it
(
'
renders bottom shadow element when `showBottomShadow` prop is true
'
,
()
=>
{
wrapper
.
setData
({
showBottomShadow
:
true
,
});
expect
(
wrapper
.
find
(
'
.epic-scroll-bottom-shadow
'
).
exists
()).
toBe
(
true
);
});
});
});
});
...
...
ee/spec/frontend/roadmap/components/roadmap_app_spec.js
View file @
7be12de8
import
{
GlAlert
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
Cookies
from
'
js-cookie
'
;
import
Cookies
from
'
js-cookie
'
;
import
{
nextTick
}
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
Vuex
from
'
vuex
'
;
import
EpicsListEmpty
from
'
ee/roadmap/components/epics_list_empty.vue
'
;
import
EpicsListEmpty
from
'
ee/roadmap/components/epics_list_empty.vue
'
;
import
RoadmapApp
from
'
ee/roadmap/components/roadmap_app.vue
'
;
import
RoadmapApp
from
'
ee/roadmap/components/roadmap_app.vue
'
;
import
RoadmapFilters
from
'
ee/roadmap/components/roadmap_filters.vue
'
;
import
RoadmapFilters
from
'
ee/roadmap/components/roadmap_filters.vue
'
;
import
RoadmapShell
from
'
ee/roadmap/components/roadmap_shell.vue
'
;
import
RoadmapShell
from
'
ee/roadmap/components/roadmap_shell.vue
'
;
import
{
PRESET_TYPES
,
EXTEND_AS
}
from
'
ee/roadmap/constants
'
;
import
{
PRESET_TYPES
}
from
'
ee/roadmap/constants
'
;
import
eventHub
from
'
ee/roadmap/event_hub
'
;
import
createStore
from
'
ee/roadmap/store
'
;
import
createStore
from
'
ee/roadmap/store
'
;
import
*
as
types
from
'
ee/roadmap/store/mutation_types
'
;
import
*
as
types
from
'
ee/roadmap/store/mutation_types
'
;
import
{
getTimeframeForMonthsView
}
from
'
ee/roadmap/utils/roadmap_utils
'
;
import
{
getTimeframeForMonthsView
}
from
'
ee/roadmap/utils/roadmap_utils
'
;
...
@@ -20,7 +18,6 @@ import {
...
@@ -20,7 +18,6 @@ import {
mockSortedBy
,
mockSortedBy
,
mockSvgPath
,
mockSvgPath
,
mockTimeframeInitialDate
,
mockTimeframeInitialDate
,
rawEpics
,
}
from
'
ee_jest/roadmap/mock_data
'
;
}
from
'
ee_jest/roadmap/mock_data
'
;
describe
(
'
RoadmapApp
'
,
()
=>
{
describe
(
'
RoadmapApp
'
,
()
=>
{
...
@@ -155,71 +152,6 @@ describe('RoadmapApp', () => {
...
@@ -155,71 +152,6 @@ describe('RoadmapApp', () => {
});
});
});
});
describe
(
'
extending the roadmap timeline
'
,
()
=>
{
let
roadmapTimelineEl
;
beforeEach
(()
=>
{
store
.
dispatch
(
'
receiveEpicsSuccess
'
,
{
rawEpics
:
rawEpics
.
slice
(
0
,
2
)
});
wrapper
=
createComponent
(
mount
);
roadmapTimelineEl
=
wrapper
.
find
(
'
.roadmap-timeline-section
'
).
element
;
});
it
(
'
updates timeline by extending timeframe from the start when called with extendType as `prepend`
'
,
()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
();
wrapper
.
vm
.
processExtendedTimeline
({
extendType
:
EXTEND_AS
.
PREPEND
,
roadmapTimelineEl
,
itemsCount
:
0
,
});
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
refreshTimeline
'
,
expect
.
any
(
Object
));
expect
(
roadmapTimelineEl
.
parentElement
.
scrollBy
).
toHaveBeenCalled
();
});
it
(
'
updates timeline by extending timeframe from the end when called with extendType as `append`
'
,
()
=>
{
jest
.
spyOn
(
eventHub
,
'
$emit
'
).
mockImplementation
();
wrapper
.
vm
.
processExtendedTimeline
({
extendType
:
EXTEND_AS
.
APPEND
,
roadmapTimelineEl
,
itemsCount
:
0
,
});
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
refreshTimeline
'
,
expect
.
any
(
Object
));
});
it
(
'
updates the store and refreshes roadmap with extended timeline based on provided extendType
'
,
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
extendTimeframe
'
).
mockImplementation
();
jest
.
spyOn
(
wrapper
.
vm
,
'
refreshEpicDates
'
).
mockImplementation
();
jest
.
spyOn
(
wrapper
.
vm
,
'
refreshMilestoneDates
'
).
mockImplementation
();
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpicsForTimeframe
'
).
mockResolvedValue
();
const
extendType
=
EXTEND_AS
.
PREPEND
;
wrapper
.
vm
.
handleScrollToExtend
({
el
:
roadmapTimelineEl
,
extendAs
:
extendType
});
expect
(
wrapper
.
vm
.
extendTimeframe
).
toHaveBeenCalledWith
({
extendAs
:
extendType
});
expect
(
wrapper
.
vm
.
refreshEpicDates
).
toHaveBeenCalled
();
expect
(
wrapper
.
vm
.
refreshMilestoneDates
).
toHaveBeenCalled
();
});
it
(
'
calls `fetchEpicsForTimeframe` with extended timeframe array
'
,
async
()
=>
{
jest
.
spyOn
(
wrapper
.
vm
,
'
fetchEpicsForTimeframe
'
).
mockResolvedValue
();
const
extendType
=
EXTEND_AS
.
PREPEND
;
wrapper
.
vm
.
handleScrollToExtend
({
el
:
roadmapTimelineEl
,
extendAs
:
extendType
});
await
nextTick
();
expect
(
wrapper
.
vm
.
fetchEpicsForTimeframe
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
timeframe
:
wrapper
.
vm
.
extendedTimeframe
,
}),
);
});
});
describe
(
'
roadmap epics limit warning
'
,
()
=>
{
describe
(
'
roadmap epics limit warning
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
();
wrapper
=
createComponent
();
...
...
ee/spec/frontend/roadmap/components/roadmap_shell_spec.js
View file @
7be12de8
...
@@ -79,12 +79,6 @@ describe('RoadmapShell', () => {
...
@@ -79,12 +79,6 @@ describe('RoadmapShell', () => {
store
=
null
;
store
=
null
;
});
});
describe
(
'
data
'
,
()
=>
{
it
(
'
returns default data props
'
,
()
=>
{
expect
(
wrapper
.
vm
.
timeframeStartOffset
).
toBe
(
0
);
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
methods
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
document
.
body
.
innerHTML
+=
document
.
body
.
innerHTML
+=
...
...
ee/spec/frontend/roadmap/store/actions_spec.js
View file @
7be12de8
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
{
PRESET_TYPES
,
EXTEND_AS
}
from
'
ee/roadmap/constants
'
;
import
{
PRESET_TYPES
}
from
'
ee/roadmap/constants
'
;
import
groupMilestones
from
'
ee/roadmap/queries/groupMilestones.query.graphql
'
;
import
groupMilestones
from
'
ee/roadmap/queries/groupMilestones.query.graphql
'
;
import
*
as
actions
from
'
ee/roadmap/store/actions
'
;
import
*
as
actions
from
'
ee/roadmap/store/actions
'
;
import
*
as
types
from
'
ee/roadmap/store/mutation_types
'
;
import
*
as
types
from
'
ee/roadmap/store/mutation_types
'
;
...
@@ -14,9 +14,7 @@ import {
...
@@ -14,9 +14,7 @@ import {
mockGroupId
,
mockGroupId
,
basePath
,
basePath
,
mockTimeframeInitialDate
,
mockTimeframeInitialDate
,
mockTimeframeMonthsPrepend
,
mockTimeframeMonthsAppend
,
mockTimeframeMonthsAppend
,
rawEpics
,
mockRawEpic
,
mockRawEpic
,
mockRawEpic2
,
mockRawEpic2
,
mockFormattedEpic
,
mockFormattedEpic
,
...
@@ -219,104 +217,6 @@ describe('Roadmap Vuex Actions', () => {
...
@@ -219,104 +217,6 @@ describe('Roadmap Vuex Actions', () => {
});
});
});
});
describe
(
'
fetchEpicsForTimeframe
'
,
()
=>
{
describe
(
'
success
'
,
()
=>
{
it
(
'
should perform REQUEST_EPICS_FOR_TIMEFRAME mutation and dispatch receiveEpicsSuccess action when request is successful
'
,
()
=>
{
jest
.
spyOn
(
epicUtils
.
gqClient
,
'
query
'
).
mockReturnValue
(
Promise
.
resolve
({
data
:
mockGroupEpicsQueryResponse
.
data
,
}),
);
return
testAction
(
actions
.
fetchEpicsForTimeframe
,
{
timeframe
:
mockTimeframeMonths
},
state
,
[
{
type
:
types
.
REQUEST_EPICS_FOR_TIMEFRAME
,
},
],
[
{
type
:
'
receiveEpicsSuccess
'
,
payload
:
{
rawEpics
:
mockGroupEpics
,
pageInfo
:
mockPageInfo
,
newEpic
:
true
,
timeframeExtended
:
true
,
},
},
],
);
});
});
describe
(
'
failure
'
,
()
=>
{
it
(
'
should perform REQUEST_EPICS_FOR_TIMEFRAME mutation and dispatch requestEpicsFailure action when request fails
'
,
()
=>
{
jest
.
spyOn
(
epicUtils
.
gqClient
,
'
query
'
).
mockRejectedValue
();
return
testAction
(
actions
.
fetchEpicsForTimeframe
,
{
timeframe
:
mockTimeframeMonths
},
state
,
[
{
type
:
types
.
REQUEST_EPICS_FOR_TIMEFRAME
,
},
],
[
{
type
:
'
receiveEpicsFailure
'
,
},
],
);
});
});
});
describe
(
'
extendTimeframe
'
,
()
=>
{
it
(
'
should prepend to timeframe when called with extend type prepend
'
,
()
=>
{
return
testAction
(
actions
.
extendTimeframe
,
{
extendAs
:
EXTEND_AS
.
PREPEND
},
state
,
[{
type
:
types
.
PREPEND_TIMEFRAME
,
payload
:
mockTimeframeMonthsPrepend
}],
[],
);
});
it
(
'
should append to timeframe when called with extend type append
'
,
()
=>
{
return
testAction
(
actions
.
extendTimeframe
,
{
extendAs
:
EXTEND_AS
.
APPEND
},
state
,
[{
type
:
types
.
APPEND_TIMEFRAME
,
payload
:
mockTimeframeMonthsAppend
}],
[],
);
});
});
describe
(
'
refreshEpicDates
'
,
()
=>
{
it
(
'
should update epics after refreshing epic dates to match with updated timeframe
'
,
()
=>
{
const
epics
=
rawEpics
.
map
((
epic
)
=>
roadmapItemUtils
.
formatRoadmapItemDetails
(
epic
,
state
.
timeframeStartDate
,
state
.
timeframeEndDate
,
),
);
return
testAction
(
actions
.
refreshEpicDates
,
{},
{
...
state
,
timeframe
:
mockTimeframeMonths
.
concat
(
mockTimeframeMonthsAppend
),
epics
},
[{
type
:
types
.
SET_EPICS
,
payload
:
epics
}],
[],
);
});
});
describe
(
'
requestChildrenEpics
'
,
()
=>
{
describe
(
'
requestChildrenEpics
'
,
()
=>
{
const
parentItemId
=
'
41
'
;
const
parentItemId
=
'
41
'
;
it
(
'
should set `itemChildrenFetchInProgress` in childrenFlags for parentItem to true
'
,
()
=>
{
it
(
'
should set `itemChildrenFetchInProgress` in childrenFlags for parentItem to true
'
,
()
=>
{
...
...
ee/spec/requests/api/graphql/epics/epic_issues_resolver_spec.rb
deleted
100644 → 0
View file @
28e47ff8
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
'getting epic issues information'
do
include
GraphqlHelpers
let_it_be
(
:user
)
{
create
(
:user
)
}
let_it_be
(
:group
)
{
create
(
:group
)
}
let_it_be
(
:project
)
{
create
(
:project
,
group:
group
)
}
let_it_be
(
:project
)
{
create
(
:project
,
group:
group
)
}
before_all
do
group
.
add_maintainer
(
user
)
end
before
do
stub_licensed_features
(
epics:
true
)
end
context
'when toggling performance_roadmap feature flag'
do
let_it_be
(
:epic
)
{
create
(
:epic
,
group:
group
,
state:
'opened'
)
}
let_it_be
(
:issue1
)
{
create
(
:issue
,
project:
project
)
}
let_it_be
(
:issue2
)
{
create
(
:issue
,
project:
project
)
}
let
(
:query
)
do
epic_issues_query
(
epic
)
end
before_all
do
create
(
:epic_issue
,
epic:
epic
,
issue:
issue1
)
create
(
:epic_issue
,
epic:
epic
,
issue:
issue2
)
end
def
epic_issues_query
(
epic
)
epic_issues_fragment
=
<<~
EPIC_ISSUE
issues {
nodes {
id
}
}
EPIC_ISSUE
graphql_query_for
(
:group
,
{
'fullPath'
=>
epic
.
group
.
full_path
},
query_graphql_field
(
'epic'
,
{
iid:
epic
.
iid
},
epic_issues_fragment
)
)
end
def
epic_issues_count
graphql_data_at
(
:group
,
:epic
,
:issues
,
:nodes
).
count
end
it
'returns epics with max page based on feature flag status'
do
stub_const
(
'SetsMaxPageSize::DEPRECATED_MAX_PAGE_SIZE'
,
1
)
post_graphql
(
epic_issues_query
(
epic
),
current_user:
user
)
expect
(
epic_issues_count
).
to
eq
(
2
)
stub_feature_flags
(
performance_roadmap:
false
)
post_graphql
(
epic_issues_query
(
epic
),
current_user:
user
)
expect
(
epic_issues_count
).
to
eq
(
1
)
stub_feature_flags
(
performance_roadmap:
true
)
post_graphql
(
epic_issues_query
(
epic
),
current_user:
user
)
expect
(
epic_issues_count
).
to
eq
(
2
)
end
end
end
ee/spec/requests/api/graphql/epics/epic_resolver_spec.rb
View file @
7be12de8
...
@@ -13,30 +13,6 @@ RSpec.describe 'getting epics information' do
...
@@ -13,30 +13,6 @@ RSpec.describe 'getting epics information' do
stub_licensed_features
(
epics:
true
)
stub_licensed_features
(
epics:
true
)
end
end
context
'when performance_roadmap flag is disabled'
do
let_it_be
(
:epic1
)
{
create
(
:epic
,
group:
group
,
state:
'opened'
)
}
let_it_be
(
:epic2
)
{
create
(
:epic
,
group:
group
,
state:
'opened'
)
}
def
epics_count
graphql_data_at
(
:group
,
:epics
,
:nodes
).
count
end
it
'returns epics within timeframe'
do
stub_const
(
'SetsMaxPageSize::DEPRECATED_MAX_PAGE_SIZE'
,
1
)
post_graphql
(
epics_query_by_hash
(
group
),
current_user:
user
)
expect
(
epics_count
).
to
eq
(
2
)
stub_feature_flags
(
performance_roadmap:
false
)
post_graphql
(
epics_query_by_hash
(
group
),
current_user:
user
)
expect
(
epics_count
).
to
eq
(
1
)
stub_feature_flags
(
performance_roadmap:
true
)
post_graphql
(
epics_query_by_hash
(
group
),
current_user:
user
)
expect
(
epics_count
).
to
eq
(
2
)
end
end
describe
'query for epics which start with an iid'
do
describe
'query for epics which start with an iid'
do
let_it_be
(
:epic1
)
{
create
(
:epic
,
group:
group
,
iid:
11
)
}
let_it_be
(
:epic1
)
{
create
(
:epic
,
group:
group
,
iid:
11
)
}
let_it_be
(
:epic2
)
{
create
(
:epic
,
group:
group
,
iid:
22
)
}
let_it_be
(
:epic2
)
{
create
(
:epic
,
group:
group
,
iid:
22
)
}
...
...
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