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
0e4094dd
Commit
0e4094dd
authored
May 27, 2021
by
Florie Guibert
Committed by
Natalia Tepluhina
May 27, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Epic sidebar widget using Apollo [RUN AS-IF-FOSS]
parent
91596934
Changes
22
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
1122 additions
and
547 deletions
+1122
-547
app/assets/javascripts/boards/components/board_content_sidebar.vue
...s/javascripts/boards/components/board_content_sidebar.vue
+11
-3
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+2
-1
ee/app/assets/javascripts/boards/components/sidebar/board_sidebar_epic_select.vue
...s/boards/components/sidebar/board_sidebar_epic_select.vue
+0
-127
ee/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
...avascripts/sidebar/components/sidebar_dropdown_widget.vue
+339
-0
ee/app/assets/javascripts/sidebar/constants.js
ee/app/assets/javascripts/sidebar/constants.js
+38
-0
ee/app/assets/javascripts/sidebar/mount_sidebar.js
ee/app/assets/javascripts/sidebar/mount_sidebar.js
+17
-13
ee/app/assets/javascripts/sidebar/queries/epic.fragment.graphql
.../assets/javascripts/sidebar/queries/epic.fragment.graphql
+5
-0
ee/app/assets/javascripts/sidebar/queries/group_epics.query.graphql
...ets/javascripts/sidebar/queries/group_epics.query.graphql
+12
-0
ee/app/assets/javascripts/sidebar/queries/project_issue_epic.mutation.graphql
...ripts/sidebar/queries/project_issue_epic.mutation.graphql
+17
-0
ee/app/assets/javascripts/sidebar/queries/project_issue_epic.query.graphql
...ascripts/sidebar/queries/project_issue_epic.query.graphql
+14
-0
ee/app/views/shared/issuable/_sidebar_item_epic.haml
ee/app/views/shared/issuable/_sidebar_item_epic.haml
+2
-1
ee/spec/features/boards/sidebar_spec.rb
ee/spec/features/boards/sidebar_spec.rb
+2
-1
ee/spec/features/issues/epic_in_issue_sidebar_spec.rb
ee/spec/features/issues/epic_in_issue_sidebar_spec.rb
+24
-24
ee/spec/features/projects/issues/user_creates_issue_spec.rb
ee/spec/features/projects/issues/user_creates_issue_spec.rb
+2
-2
ee/spec/frontend/boards/components/__snapshots__/board_content_sidebar_spec.js.snap
...mponents/__snapshots__/board_content_sidebar_spec.js.snap
+23
-48
ee/spec/frontend/boards/components/board_content_sidebar_spec.js
.../frontend/boards/components/board_content_sidebar_spec.js
+16
-19
ee/spec/frontend/boards/components/sidebar/board_sidebar_epic_select_spec.js
...ards/components/sidebar/board_sidebar_epic_select_spec.js
+0
-292
ee/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
...ontend/sidebar/components/sidebar_dropdown_widget_spec.js
+500
-0
ee/spec/frontend/sidebar/components/sidebar_iteration_widget_spec.js
...ntend/sidebar/components/sidebar_iteration_widget_spec.js
+2
-2
ee/spec/frontend/sidebar/mock_data.js
ee/spec/frontend/sidebar/mock_data.js
+69
-1
ee/spec/support/shared_examples/features/sidebar_shared_examples.rb
...pport/shared_examples/features/sidebar_shared_examples.rb
+9
-7
locale/gitlab.pot
locale/gitlab.pot
+18
-6
No files found.
app/assets/javascripts/boards/components/board_content_sidebar.vue
View file @
0e4094dd
...
...
@@ -24,12 +24,12 @@ export default {
BoardSidebarDueDate
,
SidebarSubscriptionsWidget
,
BoardSidebarMilestoneSelect
,
BoardSidebarEpicSelect
:
()
=>
import
(
'
ee_component/boards/components/sidebar/board_sidebar_epic_select.vue
'
),
BoardSidebarWeightInput
:
()
=>
import
(
'
ee_component/boards/components/sidebar/board_sidebar_weight_input.vue
'
),
SidebarIterationWidget
:
()
=>
import
(
'
ee_component/sidebar/components/sidebar_iteration_widget.vue
'
),
SidebarDropdownWidget
:
()
=>
import
(
'
ee_component/sidebar/components/sidebar_dropdown_widget.vue
'
),
},
inject
:
{
multipleAssigneesFeatureAvailable
:
{
...
...
@@ -89,7 +89,15 @@ export default {
:allow-multiple-assignees=
"multipleAssigneesFeatureAvailable"
@
assignees-updated=
"setAssignees"
/>
<board-sidebar-epic-select
v-if=
"epicFeatureAvailable"
class=
"epic"
/>
<sidebar-dropdown-widget
v-if=
"epicFeatureAvailable"
:iid=
"activeBoardItem.iid"
issuable-attribute=
"epic"
:workspace-path=
"projectPathForActiveIssue"
:attr-workspace-path=
"groupPathForActiveIssue"
:issuable-type=
"issuableType"
data-testid=
"sidebar-epic"
/>
<div>
<board-sidebar-milestone-select
/>
<sidebar-iteration-widget
...
...
app/views/shared/issuable/_sidebar.html.haml
View file @
0e4094dd
...
...
@@ -30,7 +30,8 @@
.block.reviewer.qa-reviewer-block
=
render
"shared/issuable/sidebar_reviewers"
,
issuable_sidebar:
issuable_sidebar
,
reviewers:
reviewers
,
signed_in:
signed_in
=
render_if_exists
'shared/issuable/sidebar_item_epic'
,
issuable_sidebar:
issuable_sidebar
-
if
@project
.
group
.
present?
=
render_if_exists
'shared/issuable/sidebar_item_epic'
,
issuable_sidebar:
issuable_sidebar
,
group_path:
@project
.
group
.
full_path
,
project_path:
issuable_sidebar
[
:project_full_path
],
issue_iid:
issuable_sidebar
[
:iid
],
issuable_type:
issuable_type
-
if
issuable_sidebar
[
:supports_milestone
]
-
milestone
=
issuable_sidebar
[
:milestone
]
||
{}
...
...
ee/app/assets/javascripts/boards/components/sidebar/board_sidebar_epic_select.vue
deleted
100644 → 0
View file @
91596934
<
script
>
import
{
GlLink
}
from
'
@gitlab/ui
'
;
import
{
mapState
,
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
EpicsSelect
from
'
ee/vue_shared/components/sidebar/epics_select/base.vue
'
;
import
BoardEditableItem
from
'
~/boards/components/sidebar/board_editable_item.vue
'
;
import
createFlash
from
'
~/flash
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
fullEpicId
}
from
'
../../boards_util
'
;
export
default
{
components
:
{
BoardEditableItem
,
EpicsSelect
,
GlLink
,
},
i18n
:
{
epic
:
__
(
'
Epic
'
),
updateEpicError
:
s__
(
'
IssueBoards|An error occurred while assigning the selected epic to the issue.
'
,
),
fetchEpicError
:
s__
(
'
IssueBoards|An error occurred while fetching the assigned epic of the selected issue.
'
,
),
},
inject
:
[
'
groupId
'
],
computed
:
{
...
mapState
([
'
epics
'
,
'
epicsCacheById
'
,
'
epicFetchInProgress
'
]),
...
mapGetters
([
'
activeBoardItem
'
,
'
projectPathForActiveIssue
'
]),
epic
()
{
return
this
.
activeBoardItem
.
epic
;
},
epicData
()
{
const
hasEpic
=
this
.
epic
!==
null
;
const
epicFetched
=
!
this
.
epicFetchInProgress
;
return
hasEpic
&&
epicFetched
?
this
.
epicsCacheById
[
this
.
epic
.
id
]
:
{};
},
initialEpic
()
{
return
this
.
epic
?
{
...
this
.
epicData
,
id
:
getIdFromGraphQLId
(
this
.
epic
.
id
),
}
:
{};
},
},
watch
:
{
epic
:
{
deep
:
true
,
immediate
:
true
,
async
handler
()
{
if
(
this
.
epic
)
{
try
{
await
this
.
fetchEpicForActiveIssue
();
}
catch
(
e
)
{
createFlash
({
message
:
this
.
$options
.
i18n
.
fetchEpicError
,
error
:
e
,
captureError
:
true
,
});
}
}
},
},
},
methods
:
{
...
mapActions
([
'
setActiveIssueEpic
'
,
'
fetchEpicForActiveIssue
'
]),
handleOpen
()
{
if
(
!
this
.
epicFetchInProgress
)
{
this
.
$refs
.
epicSelect
.
toggleFormDropdown
();
}
else
{
this
.
$refs
.
sidebarItem
.
collapse
();
}
},
handleClose
()
{
this
.
$refs
.
sidebarItem
.
collapse
();
this
.
$refs
.
epicSelect
.
toggleFormDropdown
();
},
async
setEpic
(
selectedEpic
)
{
this
.
handleClose
();
const
epicId
=
selectedEpic
?.
id
?
fullEpicId
(
selectedEpic
.
id
)
:
null
;
const
assignedEpicId
=
this
.
epic
?.
id
?
fullEpicId
(
this
.
epic
.
id
)
:
null
;
if
(
epicId
===
assignedEpicId
)
{
return
;
}
try
{
await
this
.
setActiveIssueEpic
(
epicId
);
}
catch
(
e
)
{
createFlash
({
message
:
this
.
$options
.
i18n
.
updateEpicError
,
error
:
e
,
captureError
:
true
});
}
},
},
};
</
script
>
<
template
>
<board-editable-item
ref=
"sidebarItem"
:title=
"$options.i18n.epic"
:loading=
"epicFetchInProgress"
data-testid=
"sidebar-epic"
@
open=
"handleOpen"
@
close=
"handleClose"
>
<template
v-if=
"epicData.title"
#collapsed
>
<gl-link
class=
"gl-text-gray-900! gl-font-weight-bold"
:href=
"epicData.webPath"
>
{{
epicData
.
title
}}
</gl-link>
</
template
>
<epics-select
v-if=
"!epicFetchInProgress"
ref=
"epicSelect"
class=
"gl-w-full"
:group-id=
"groupId"
:can-edit=
"true"
:initial-epic=
"initialEpic"
:initial-epic-loading=
"false"
variant=
"standalone"
:show-header=
"false"
@
epicSelect=
"setEpic"
@
hide=
"handleClose"
/>
</board-editable-item>
</template>
ee/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
0 → 100644
View file @
0e4094dd
<
script
>
import
{
GlLink
,
GlDropdown
,
GlDropdownItem
,
GlDropdownText
,
GlSearchBoxByType
,
GlDropdownDivider
,
GlLoadingIcon
,
GlIcon
,
GlTooltipDirective
,
}
from
'
@gitlab/ui
'
;
import
createFlash
from
'
~/flash
'
;
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
{
__
,
s__
,
sprintf
}
from
'
~/locale
'
;
import
SidebarEditableItem
from
'
~/sidebar/components/sidebar_editable_item.vue
'
;
import
{
IssuableAttributeState
,
IssuableAttributeType
,
issuableAttributesQueries
,
noAttributeId
,
}
from
'
../constants
'
;
export
default
{
noAttributeId
,
i18n
:
{
[
IssuableAttributeType
.
Iteration
]:
__
(
'
Iteration
'
),
[
IssuableAttributeType
.
Epic
]:
__
(
'
Epic
'
),
none
:
__
(
'
None
'
),
},
directives
:
{
GlTooltip
:
GlTooltipDirective
,
},
components
:
{
SidebarEditableItem
,
GlLink
,
GlDropdown
,
GlDropdownItem
,
GlDropdownText
,
GlDropdownDivider
,
GlSearchBoxByType
,
GlIcon
,
GlLoadingIcon
,
},
inject
:
{
isClassicSidebar
:
{
default
:
false
,
},
},
props
:
{
issuableAttribute
:
{
type
:
String
,
required
:
true
,
validator
(
value
)
{
return
[
IssuableAttributeType
.
Iteration
,
IssuableAttributeType
.
Epic
].
includes
(
value
);
},
},
workspacePath
:
{
required
:
true
,
type
:
String
,
},
iid
:
{
required
:
true
,
type
:
String
,
},
attrWorkspacePath
:
{
required
:
true
,
type
:
String
,
},
issuableType
:
{
type
:
String
,
required
:
true
,
validator
(
value
)
{
return
value
===
IssuableType
.
Issue
;
},
},
},
apollo
:
{
currentAttribute
:
{
query
()
{
const
{
current
}
=
this
.
issuableAttributeQuery
;
const
{
query
}
=
current
[
this
.
issuableType
];
return
query
;
},
variables
()
{
return
{
fullPath
:
this
.
workspacePath
,
iid
:
this
.
iid
,
};
},
update
(
data
)
{
return
data
?.
workspace
?.
issuable
.
attribute
;
},
error
(
error
)
{
createFlash
({
message
:
this
.
i18n
.
currentFetchError
,
captureError
:
true
,
error
,
});
},
},
attributesList
:
{
query
()
{
const
{
list
}
=
this
.
issuableAttributeQuery
;
const
{
query
}
=
list
[
this
.
issuableType
];
return
query
;
},
skip
()
{
return
!
this
.
editing
;
},
debounce
:
250
,
variables
()
{
return
{
fullPath
:
this
.
attrWorkspacePath
,
title
:
this
.
searchTerm
,
state
:
IssuableAttributeState
[
this
.
issuableAttribute
],
};
},
update
(
data
)
{
if
(
data
?.
workspace
)
{
return
data
?.
workspace
?.
attributes
.
nodes
;
}
return
[];
},
error
(
error
)
{
createFlash
({
message
:
this
.
i18n
.
listFetchError
,
captureError
:
true
,
error
});
},
},
},
data
()
{
return
{
searchTerm
:
''
,
editing
:
false
,
updating
:
false
,
selectedTitle
:
null
,
currentAttribute
:
null
,
attributesList
:
[],
tracking
:
{
label
:
'
right_sidebar
'
,
event
:
'
click_edit_button
'
,
property
:
this
.
issuableAttribute
,
},
};
},
computed
:
{
issuableAttributeQuery
()
{
return
issuableAttributesQueries
[
this
.
issuableAttribute
];
},
attributeTitle
()
{
return
this
.
currentAttribute
?.
title
||
this
.
i18n
.
noAttribute
;
},
attributeUrl
()
{
return
this
.
currentAttribute
?.
webUrl
;
},
dropdownText
()
{
return
this
.
currentAttribute
?
this
.
currentAttribute
?.
title
:
this
.
$options
.
i18n
[
this
.
issuableAttribute
];
},
loading
()
{
return
this
.
$apollo
.
queries
.
currentAttribute
.
loading
;
},
emptyPropsList
()
{
return
this
.
attributesList
.
length
===
0
;
},
attributeTypeTitle
()
{
return
this
.
$options
.
i18n
[
this
.
issuableAttribute
];
},
i18n
()
{
return
{
noAttribute
:
sprintf
(
s__
(
'
DropdownWidget|No %{issuableAttribute}
'
),
{
issuableAttribute
:
this
.
issuableAttribute
,
}),
assignAttribute
:
sprintf
(
s__
(
'
DropdownWidget|Assign %{issuableAttribute}
'
),
{
issuableAttribute
:
this
.
issuableAttribute
,
}),
noAttributesFound
:
sprintf
(
s__
(
'
DropdownWidget|No %{issuableAttribute} found
'
),
{
issuableAttribute
:
this
.
issuableAttribute
,
}),
updateError
:
sprintf
(
s__
(
'
DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again.
'
,
),
{
issuableAttribute
:
this
.
issuableAttribute
,
issuableType
:
this
.
issuableType
},
),
listFetchError
:
sprintf
(
s__
(
'
DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again.
'
,
),
{
issuableAttribute
:
this
.
issuableAttribute
,
issuableType
:
this
.
issuableType
},
),
currentFetchError
:
sprintf
(
s__
(
'
DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}.
'
,
),
{
issuableAttribute
:
this
.
issuableAttribute
,
issuableType
:
this
.
issuableType
},
),
};
},
},
methods
:
{
updateAttribute
(
attributeId
)
{
if
(
this
.
currentAttribute
===
null
&&
attributeId
===
null
)
return
;
if
(
attributeId
===
this
.
currentAttribute
?.
id
)
return
;
this
.
updating
=
true
;
const
selectedAttribute
=
Boolean
(
attributeId
)
&&
this
.
attributesList
.
find
((
p
)
=>
p
.
id
===
attributeId
);
this
.
selectedTitle
=
selectedAttribute
?
selectedAttribute
.
title
:
this
.
$options
.
i18n
.
none
;
const
{
current
}
=
this
.
issuableAttributeQuery
;
const
{
mutation
}
=
current
[
this
.
issuableType
];
this
.
$apollo
.
mutate
({
mutation
,
variables
:
{
fullPath
:
this
.
workspacePath
,
attributeId
,
iid
:
this
.
iid
,
},
})
.
then
(({
data
})
=>
{
if
(
data
.
issuableSetAttribute
?.
errors
?.
length
)
{
createFlash
({
message
:
data
.
issuableSetAttribute
.
errors
[
0
],
captureError
:
true
,
error
:
data
.
issuableSetAttribute
.
errors
[
0
],
});
}
else
{
this
.
$emit
(
'
attribute-updated
'
,
data
);
}
})
.
catch
((
error
)
=>
{
createFlash
({
message
:
this
.
i18n
.
updateError
,
captureError
:
true
,
error
});
})
.
finally
(()
=>
{
this
.
updating
=
false
;
this
.
searchTerm
=
''
;
this
.
selectedTitle
=
null
;
});
},
isAttributeChecked
(
attributeId
=
undefined
)
{
return
(
attributeId
===
this
.
currentAttribute
?.
id
||
(
!
this
.
currentAttribute
?.
id
&&
!
attributeId
)
);
},
showDropdown
()
{
this
.
$refs
.
newDropdown
.
show
();
},
handleOpen
()
{
this
.
editing
=
true
;
this
.
showDropdown
();
},
handleClose
()
{
this
.
editing
=
false
;
},
setFocus
()
{
this
.
$refs
.
search
.
focusInput
();
},
},
};
</
script
>
<
template
>
<sidebar-editable-item
ref=
"editable"
:title=
"attributeTypeTitle"
:data-testid=
"`$
{issuableAttribute}-edit`"
:tracking="tracking"
:loading="updating || loading"
@open="handleOpen"
@close="handleClose"
>
<template
#collapsed
>
<div
v-if=
"isClassicSidebar"
v-gl-tooltip
class=
"sidebar-collapsed-icon"
>
<gl-icon
:size=
"16"
:aria-label=
"attributeTypeTitle"
:name=
"issuableAttribute"
/>
<span
class=
"collapse-truncated-title"
>
{{
attributeTitle
}}
</span>
</div>
<div
:data-testid=
"`select-$
{issuableAttribute}`"
:class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'"
>
<span
v-if=
"updating"
class=
"gl-font-weight-bold"
>
{{
selectedTitle
}}
</span>
<span
v-else-if=
"!currentAttribute"
class=
"gl-text-gray-500"
>
{{
$options
.
i18n
.
none
}}
</span>
<gl-link
v-else
class=
"gl-text-gray-900! gl-font-weight-bold"
:href=
"attributeUrl"
>
{{
attributeTitle
}}
</gl-link>
</div>
</
template
>
<
template
#default
>
<gl-dropdown
ref=
"newDropdown"
lazy
:header-text=
"i18n.assignAttribute"
:text=
"dropdownText"
:loading=
"loading"
class=
"gl-w-full"
@
shown=
"setFocus"
>
<gl-search-box-by-type
ref=
"search"
v-model=
"searchTerm"
/>
<gl-dropdown-item
:data-testid=
"`no-$
{issuableAttribute}-item`"
:is-check-item="true"
:is-checked="isAttributeChecked($options.noAttributeId)"
@click="updateAttribute($options.noAttributeId)"
>
{{
i18n
.
noAttribute
}}
</gl-dropdown-item>
<gl-dropdown-divider
/>
<gl-loading-icon
v-if=
"$apollo.queries.attributesList.loading"
class=
"gl-py-4"
data-testid=
"loading-icon-dropdown"
/>
<template
v-else
>
<gl-dropdown-text
v-if=
"emptyPropsList"
>
{{
i18n
.
noAttributesFound
}}
</gl-dropdown-text>
<gl-dropdown-item
v-for=
"attrItem in attributesList"
:key=
"attrItem.id"
:is-check-item=
"true"
:is-checked=
"isAttributeChecked(attrItem.id)"
:data-testid=
"`$
{issuableAttribute}-items`"
@click="updateAttribute(attrItem.id)"
>
{{
attrItem
.
title
}}
</gl-dropdown-item>
</
template
>
</gl-dropdown>
</template>
</sidebar-editable-item>
</template>
ee/app/assets/javascripts/sidebar/constants.js
View file @
0e4094dd
import
{
IssuableType
}
from
'
~/issue_show/constants
'
;
import
{
s__
,
__
}
from
'
~/locale
'
;
import
groupEpicsQuery
from
'
./queries/group_epics.query.graphql
'
;
import
groupIterationsQuery
from
'
./queries/group_iterations.query.graphql
'
;
import
projectIssueEpicMutation
from
'
./queries/project_issue_epic.mutation.graphql
'
;
import
projectIssueEpicQuery
from
'
./queries/project_issue_epic.query.graphql
'
;
import
projectIssueIterationMutation
from
'
./queries/project_issue_iteration.mutation.graphql
'
;
import
projectIssueIterationQuery
from
'
./queries/project_issue_iteration.query.graphql
'
;
...
...
@@ -31,6 +34,7 @@ export const iterationSelectTextMap = {
};
export
const
noIteration
=
null
;
export
const
noAttributeId
=
null
;
export
const
iterationDisplayState
=
'
opened
'
;
...
...
@@ -76,3 +80,37 @@ export const iterationsQueries = {
query
:
groupIterationsQuery
,
},
};
const
issuableEpicQueries
=
{
[
IssuableType
.
Issue
]:
{
query
:
projectIssueEpicQuery
,
mutation
:
projectIssueEpicMutation
,
},
};
const
epicsQueries
=
{
[
IssuableType
.
Issue
]:
{
query
:
groupEpicsQuery
,
},
};
export
const
IssuableAttributeType
=
{
Iteration
:
'
iteration
'
,
Epic
:
'
epic
'
,
};
export
const
IssuableAttributeState
=
{
[
IssuableAttributeType
.
Iteration
]:
'
opened
'
,
[
IssuableAttributeType
.
Epic
]:
'
opened
'
,
};
export
const
issuableAttributesQueries
=
{
[
IssuableAttributeType
.
Iteration
]:
{
current
:
issuableIterationQueries
,
list
:
iterationsQueries
,
},
[
IssuableAttributeType
.
Epic
]:
{
current
:
issuableEpicQueries
,
list
:
epicsQueries
,
},
};
ee/app/assets/javascripts/sidebar/mount_sidebar.js
View file @
0e4094dd
...
...
@@ -6,11 +6,11 @@ import { store } from '~/notes/stores';
import
{
apolloProvider
}
from
'
~/sidebar/graphql
'
;
import
*
as
CEMountSidebar
from
'
~/sidebar/mount_sidebar
'
;
import
CveIdRequest
from
'
./components/cve_id_request/cve_id_request_sidebar.vue
'
;
import
Sidebar
ItemEpicsSelect
from
'
./components/sidebar_item_epics_selec
t.vue
'
;
import
Sidebar
DropdownWidget
from
'
./components/sidebar_dropdown_widge
t.vue
'
;
import
SidebarIterationWidget
from
'
./components/sidebar_iteration_widget.vue
'
;
import
SidebarStatus
from
'
./components/status/sidebar_status.vue
'
;
import
SidebarWeight
from
'
./components/weight/sidebar_weight.vue
'
;
import
SidebarStore
from
'
./stores/sidebar_store
'
;
import
{
IssuableAttributeType
}
from
'
./constants
'
;
Vue
.
use
(
VueApollo
);
...
...
@@ -75,31 +75,35 @@ function mountCveIdRequestComponent() {
});
}
const
mountEpicsSelect
=
()
=>
{
function
mountEpicsSelect
()
{
const
el
=
document
.
querySelector
(
'
#js-vue-sidebar-item-epics-select
'
);
if
(
!
el
)
return
false
;
const
{
groupId
,
issueId
,
epicIssueId
,
canEdit
}
=
el
.
dataset
;
const
sidebarStore
=
new
SidebarStore
();
const
{
groupPath
,
canEdit
,
projectPath
,
issueIid
}
=
el
.
dataset
;
return
new
Vue
({
el
,
apolloProvider
,
components
:
{
SidebarItemEpicsSelect
,
SidebarDropdownWidget
,
},
provide
:
{
canUpdate
:
parseBoolean
(
canEdit
),
isClassicSidebar
:
true
,
},
render
:
(
createElement
)
=>
createElement
(
'
sidebar-
item-epics-selec
t
'
,
{
createElement
(
'
sidebar-
dropdown-widge
t
'
,
{
props
:
{
sidebarStore
,
groupId
:
Number
(
groupId
)
,
i
ssueId
:
Number
(
issueId
)
,
epicIssueId
:
Number
(
epicIssueId
)
,
canEdit
:
parseBoolean
(
canEdit
)
,
attrWorkspacePath
:
groupPath
,
workspacePath
:
projectPath
,
i
id
:
issueIid
,
issuableType
:
IssuableType
.
Issue
,
issuableAttribute
:
IssuableAttributeType
.
Epic
,
},
}),
});
}
;
}
function
mountIterationSelect
()
{
const
el
=
document
.
querySelector
(
'
.js-iteration-select
'
);
...
...
ee/app/assets/javascripts/sidebar/queries/epic.fragment.graphql
0 → 100644
View file @
0e4094dd
fragment
EpicFragment
on
Epic
{
id
title
webUrl
}
ee/app/assets/javascripts/sidebar/queries/group_epics.query.graphql
0 → 100644
View file @
0e4094dd
#import "./epic.fragment.graphql"
query
issueEpics
(
$fullPath
:
ID
!,
$title
:
String
,
$state
:
EpicState
)
{
workspace
:
group
(
fullPath
:
$fullPath
)
{
attributes
:
epics
(
search
:
$title
,
state
:
$state
)
{
nodes
{
...
EpicFragment
state
}
}
}
}
ee/app/assets/javascripts/sidebar/queries/project_issue_epic.mutation.graphql
0 → 100644
View file @
0e4094dd
mutation
projectIssueEpicMutation
(
$fullPath
:
ID
!,
$iid
:
String
!,
$attributeId
:
EpicID
)
{
issuableSetAttribute
:
issueSetEpic
(
input
:
{
projectPath
:
$fullPath
,
iid
:
$iid
,
epicId
:
$attributeId
}
)
{
__typename
errors
issuable
:
issue
{
__typename
id
attribute
:
epic
{
title
id
state
}
}
}
}
ee/app/assets/javascripts/sidebar/queries/project_issue_epic.query.graphql
0 → 100644
View file @
0e4094dd
#import "./epic.fragment.graphql"
query
projectIssueEpic
(
$fullPath
:
ID
!,
$iid
:
String
!)
{
workspace
:
project
(
fullPath
:
$fullPath
)
{
__typename
issuable
:
issue
(
iid
:
$iid
)
{
__typename
id
attribute
:
epic
{
...
EpicFragment
}
}
}
}
ee/app/views/shared/issuable/_sidebar_item_epic.haml
View file @
0e4094dd
-
if
issuable_sidebar
[
:supports_epic
]
-
if
issuable_sidebar
[
:features_available
][
:epics
]
#js-vue-sidebar-item-epics-select
{
data:
{
can_edit:
can_admin_issue?
.
to_s
,
group_id:
@project
.
group
.
id
,
issue_id:
@issuable
.
id
,
epic_issue_id:
@issuable
.
epic_issue
&
.
id
}
}
.block.epic
{
data:
{
testid:
'sidebar-epic'
}
}
#js-vue-sidebar-item-epics-select
{
data:
{
can_edit:
can_admin_issue?
.
to_s
,
group_path:
group_path
,
project_path:
project_path
,
issue_iid:
issue_iid
}
}
-
else
=
render
'shared/promotions/promote_epics'
ee/spec/features/boards/sidebar_spec.rb
View file @
0e4094dd
...
...
@@ -200,7 +200,8 @@ RSpec.describe 'Issue Boards', :js do
it
'displays name of epic and links to it'
do
click_card
(
card2
)
expect
(
epic_widget
).
to
have_link
(
epic1
.
title
,
href:
epic_path
(
epic1
))
expect
(
epic_widget
).
to
have_link
(
epic1
.
title
)
expect
(
find_link
(
epic1
.
title
)[
:href
]).
to
end_with
(
epic_path
(
epic1
))
end
it
'updates the epic associated with the issue'
do
...
...
ee/spec/features/issues/epic_in_issue_sidebar_spec.rb
View file @
0e4094dd
...
...
@@ -3,14 +3,14 @@
require
'spec_helper'
RSpec
.
describe
'Epic in issue sidebar'
,
:js
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
,
:public
)
}
let
(
:epic1
)
{
create
(
:epic
,
group:
group
,
title:
'Foo'
)
}
let
!
(
:epic2
)
{
create
(
:epic
,
group:
group
,
title:
'Bar'
)
}
let
!
(
:epic3
)
{
create
(
:epic
,
group:
group
,
title:
'Baz'
)
}
let
(
:project
)
{
create
(
:project
,
:public
,
group:
group
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let
!
(
:epic_issue
)
{
create
(
:epic_issue
,
epic:
epic1
,
issue:
issue
)
}
let
_it_be
(
:user
)
{
create
(
:user
)
}
let
_it_be
(
:group
)
{
create
(
:group
,
:public
)
}
let
_it_be
(
:epic1
)
{
create
(
:epic
,
group:
group
,
title:
'Foo'
)
}
let
_it_be
(
:epic2
)
{
create
(
:epic
,
group:
group
,
title:
'Bar'
)
}
let
_it_be
(
:epic3
)
{
create
(
:epic
,
group:
group
,
title:
'Baz'
)
}
let
_it_be
(
:project
)
{
create
(
:project
,
:public
,
group:
group
)
}
let
_it_be
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let
_it_be
(
:epic_issue
)
{
create
(
:epic_issue
,
epic:
epic1
,
issue:
issue
)
}
shared_examples
'epic in issue sidebar'
do
context
'projects within a group'
do
...
...
@@ -18,52 +18,52 @@ RSpec.describe 'Epic in issue sidebar', :js do
group
.
add_owner
(
user
)
sign_in
user
visit
project_issue_path
(
project
,
issue
)
wait_for_all_requests
end
it
'shows epic in issue sidebar'
do
expect
(
page
.
find
(
'
.js-epic-block .value
'
)).
to
have_content
(
epic1
.
title
)
expect
(
page
.
find
(
'
[data-testid="sidebar-epic"]
'
)).
to
have_content
(
epic1
.
title
)
end
it
'shows edit button in issue sidebar'
do
expect
(
page
.
find
(
'
.js-epic-block .sidebar-dropdown-toggle'
)).
to
have_content
(
'Edit'
)
expect
(
page
.
find
(
'
[data-testid="sidebar-epic"]'
)).
to
have_button
(
'Edit'
)
end
it
'shows epics select dropdown'
do
page
.
within
(
find
(
'
.js-epic-block
'
))
do
page
.
find
(
'.sidebar-dropdown-toggle'
).
click
page
.
within
(
find
(
'
[data-testid="sidebar-epic"]
'
))
do
click_button
'Edit'
wait_for_requests
wait_for_
all_
requests
expect
(
page
).
to
have_selector
(
'.js-epic-select'
,
visible:
true
)
expect
(
page
.
all
(
'.gl-new-dropdown-contents .gl-new-dropdown-item'
).
length
).
to
eq
(
4
)
# `No Epic` + 3 epics
expect
(
page
).
to
have_selector
(
'.gl-new-dropdown-contents .gl-new-dropdown-item'
,
count:
4
)
# `No Epic` + 3 epics
end
end
it
'supports searching for an epic'
do
page
.
within
(
find
(
'
.js-epic-block
'
))
do
page
.
find
(
'.sidebar-dropdown-toggle'
).
click
page
.
within
(
find
(
'
[data-testid="sidebar-epic"]
'
))
do
click_button
'Edit'
wait_for_requests
wait_for_
all_
requests
page
.
find
(
'.gl-form-input'
).
send_keys
(
'Foo'
)
wait_for_requests
wait_for_
all_
requests
expect
(
page
).
to
have_selector
(
'.gl-new-dropdown-contents .gl-new-dropdown-item'
,
count:
2
)
# `No Epic` + 1 matching epic
end
end
it
'select an epic from the dropdown'
do
page
.
within
(
find
(
'
.js-epic-block
'
))
do
page
.
find
(
'.sidebar-dropdown-toggle'
).
click
page
.
within
(
find
(
'
[data-testid="sidebar-epic"]
'
))
do
click_button
'Edit'
wait_for_requests
wait_for_
all_
requests
find
(
'.gl-new-dropdown-item'
,
text:
epic2
.
title
).
click
wait_for_requests
wait_for_
all_
requests
expect
(
page
.
find
(
'
.value
'
)).
to
have_content
(
epic2
.
title
)
expect
(
page
.
find
(
'
[data-testid="select-epic"]
'
)).
to
have_content
(
epic2
.
title
)
end
end
end
...
...
ee/spec/features/projects/issues/user_creates_issue_spec.rb
View file @
0e4094dd
...
...
@@ -53,7 +53,7 @@ RSpec.describe "User creates issue", :js do
wait_for_all_requests
page
.
within
(
".js-epic-block .js-epic-label"
)
do
page
.
within
(
'[data-testid="select-epic"]'
)
do
expect
(
page
).
to
have_content
(
'None'
)
end
...
...
@@ -67,7 +67,7 @@ RSpec.describe "User creates issue", :js do
wait_for_all_requests
page
.
within
(
".js-epic-block .js-epic-label"
)
do
page
.
within
(
'[data-testid="select-epic"]'
)
do
expect
(
page
).
to
have_content
(
epic
.
title
)
end
...
...
ee/spec/frontend/boards/components/__snapshots__/board_content_sidebar_spec.js.snap
View file @
0e4094dd
...
...
@@ -3,82 +3,57 @@
exports[`ee/BoardContentSidebar matches the snapshot 1`] = `
<div>
Issue details
<board
-sidebar-
title-stub />
<board
sidebar
title-stub />
<sidebar
-assignees-
widget-stub
<sidebar
assignees
widget-stub
fullpath="gitlab-org/gitlab-test"
iid="27"
initialassignees="[object Object],[object Object]"
issuabletype="issue"
/>
<
board-editable-item
-stub
class="epic
"
<
sidebardropdownwidget
-stub
attr-workspace-path="gitlab-org
"
data-testid="sidebar-epic"
handleoffclick="true"
title="Epic"
>
<epics-select-stub
canedit="true"
class="gl-w-full"
epicissueid="0"
groupid="1"
initialepic="[object Object]"
issueid="0"
variant="standalone"
/>
</board-editable-item-stub>
iid="27"
issuable-attribute="epic"
issuable-type="issue"
workspace-path="gitlab-org/gitlab-test"
/>
<div>
<board
-sidebar-milestone-
select-stub />
<board
sidebarmilestone
select-stub />
<
div
<
sidebariterationwidget-stub
class="gl-mt-5"
data-qa-selector="iteration_container"
>
<sidebar-editable-item-stub
canedit="true"
data-testid="iteration-edit-link"
title="Iteration"
tracking="[object Object]"
/>
</div>
iid="27"
issuable-type="issue"
iterations-workspace-path="gitlab-org"
workspace-path="gitlab-org/gitlab-test"
/>
</div>
<board
-sidebar-time-
tracker-stub
<board
sidebartime
tracker-stub
class="swimlanes-sidebar-time-tracker"
/>
<board
-sidebar-due-
date-stub />
<board
sidebardue
date-stub />
<board
-sidebar-labels-
select-stub
<board
sidebarlabels
select-stub
class="labels"
/>
<board
-editable-item
-stub
<board
sidebarweightinput
-stub
class="weight"
data-testid="sidebar-weight"
handleoffclick="true"
title="Weight"
>
<gl-form-stub>
<gl-form-input-stub
min="0"
placeholder="Enter a number"
type="number"
/>
</gl-form-stub>
</board-editable-item-stub>
/>
<sidebar
-confidentiality-
widget-stub
<sidebar
confidentiality
widget-stub
fullpath="gitlab-org/gitlab-test"
iid="27"
issuabletype="issue"
/>
<sidebar
-subscriptions-
widget-stub
<sidebar
subscriptions
widget-stub
data-testid="sidebar-notifications"
fullpath="gitlab-org/gitlab-test"
iid="27"
...
...
ee/spec/frontend/boards/components/board_content_sidebar_spec.js
View file @
0e4094dd
import
{
GlDrawer
}
from
'
@gitlab/ui
'
;
import
{
shallowM
ount
}
from
'
@vue/test-utils
'
;
import
{
m
ount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
import
BoardContentSidebar
from
'
~/boards/components/board_content_sidebar.vue
'
;
...
...
@@ -34,12 +34,12 @@ describe('ee/BoardContentSidebar', () => {
const
createComponent
=
()
=>
{
/*
Dynamically imported components (in our case ee imports)
aren't stubbed automatically in VTU v1:
aren't stubbed automatically
when using shallow mount
in VTU v1:
https://github.com/vuejs/vue-test-utils/issues/1279.
This requires us to
additionally mock apollo or vuex store
s.
This requires us to
use mount and additionally mock component
s.
*/
wrapper
=
shallowM
ount
(
BoardContentSidebar
,
{
wrapper
=
m
ount
(
BoardContentSidebar
,
{
provide
:
{
canUpdate
:
true
,
rootPath
:
'
/
'
,
...
...
@@ -53,21 +53,18 @@ describe('ee/BoardContentSidebar', () => {
GlDrawer
:
stubComponent
(
GlDrawer
,
{
template
:
'
<div><slot name="header"></slot><slot></slot></div>
'
,
}),
},
mocks
:
{
$apollo
:
{
queries
:
{
participants
:
{
loading
:
false
,
},
currentIteration
:
{
loading
:
false
,
},
iterations
:
{
loading
:
false
,
},
},
},
BoardEditableItem
:
true
,
BoardSidebarTitle
:
true
,
BoardSidebarTimeTracker
:
true
,
BoardSidebarLabelsSelect
:
true
,
SidebarAssigneesWidget
:
true
,
SidebarConfidentialityWidget
:
true
,
BoardSidebarDueDate
:
true
,
SidebarSubscriptionsWidget
:
true
,
BoardSidebarMilestoneSelect
:
true
,
BoardSidebarWeightInput
:
true
,
SidebarIterationWidget
:
true
,
SidebarDropdownWidget
:
true
,
},
});
};
...
...
ee/spec/frontend/boards/components/sidebar/board_sidebar_epic_select_spec.js
deleted
100644 → 0
View file @
91596934
import
{
GlLink
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
Vuex
from
'
vuex
'
;
import
BoardSidebarEpicSelect
from
'
ee/boards/components/sidebar/board_sidebar_epic_select.vue
'
;
import
EpicsSelect
from
'
ee/vue_shared/components/sidebar/epics_select/base.vue
'
;
import
{
stubComponent
}
from
'
helpers/stub_component
'
;
import
BoardEditableItem
from
'
~/boards/components/sidebar/board_editable_item.vue
'
;
import
getters
from
'
~/boards/stores/getters
'
;
import
createFlash
from
'
~/flash
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
{
mockIssue3
as
mockIssueWithoutEpic
,
mockIssueWithEpic
,
mockAssignedEpic
,
}
from
'
../../mock_data
'
;
jest
.
mock
(
'
~/flash
'
);
const
mockGroupId
=
7
;
describe
(
'
ee/boards/components/sidebar/board_sidebar_epic_select.vue
'
,
()
=>
{
let
wrapper
;
let
store
;
afterEach
(()
=>
{
wrapper
.
destroy
();
store
=
null
;
wrapper
=
null
;
});
const
createStore
=
({
initialState
=
{
activeId
:
mockIssueWithoutEpic
.
id
,
boardItems
:
{
[
mockIssueWithoutEpic
.
id
]:
{
...
mockIssueWithoutEpic
}
},
epicsCacheById
:
{},
epicFetchInProgress
:
false
,
},
actionsMock
=
{},
}
=
{})
=>
{
store
=
new
Vuex
.
Store
({
state
:
initialState
,
getters
,
actions
:
{
...
actionsMock
,
},
});
};
let
epicsSelectHandleEditClick
;
const
createWrapper
=
()
=>
{
epicsSelectHandleEditClick
=
jest
.
fn
();
wrapper
=
shallowMount
(
BoardSidebarEpicSelect
,
{
store
,
provide
:
{
groupId
:
mockGroupId
,
canUpdate
:
true
,
},
stubs
:
{
BoardEditableItem
,
EpicsSelect
:
stubComponent
(
EpicsSelect
,
{
methods
:
{
toggleFormDropdown
:
epicsSelectHandleEditClick
,
},
}),
},
});
};
const
findEpicSelect
=
()
=>
wrapper
.
find
({
ref
:
'
epicSelect
'
});
const
findItemWrapper
=
()
=>
wrapper
.
find
({
ref
:
'
sidebarItem
'
});
const
findCollapsed
=
()
=>
wrapper
.
find
(
'
[data-testid="collapsed-content"]
'
);
const
findEpicLink
=
()
=>
wrapper
.
find
(
GlLink
);
const
findBoardEditableItem
=
()
=>
wrapper
.
find
(
BoardEditableItem
);
describe
(
'
when not editing
'
,
()
=>
{
it
(
'
expands the milestone dropdown on clicking edit
'
,
async
()
=>
{
createStore
();
createWrapper
();
await
findBoardEditableItem
().
vm
.
$emit
(
'
open
'
);
expect
(
epicsSelectHandleEditClick
).
toHaveBeenCalled
();
});
});
describe
(
'
when editing
'
,
()
=>
{
beforeEach
(()
=>
{
createStore
();
createWrapper
();
findItemWrapper
().
vm
.
$emit
(
'
open
'
);
jest
.
spyOn
(
wrapper
.
vm
.
$refs
.
sidebarItem
,
'
collapse
'
);
});
it
(
'
collapses BoardEditableItem on clicking edit
'
,
async
()
=>
{
await
findBoardEditableItem
().
vm
.
$emit
(
'
close
'
);
expect
(
wrapper
.
vm
.
$refs
.
sidebarItem
.
collapse
).
toHaveBeenCalledTimes
(
1
);
});
it
(
'
collapses BoardEditableItem on hiding dropdown
'
,
async
()
=>
{
await
wrapper
.
find
(
EpicsSelect
).
vm
.
$emit
(
'
hide
'
);
expect
(
wrapper
.
vm
.
$refs
.
sidebarItem
.
collapse
).
toHaveBeenCalledTimes
(
1
);
});
});
it
(
'
renders "None" when no epic is assigned to the active issue
'
,
async
()
=>
{
createStore
();
createWrapper
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
findCollapsed
().
text
()).
toBe
(
'
None
'
);
});
describe
(
'
when active issue has an assigned epic
'
,
()
=>
{
it
(
'
fetches an epic for active issue
'
,
()
=>
{
const
fetchEpicForActiveIssue
=
jest
.
fn
(()
=>
Promise
.
resolve
());
createStore
({
initialState
:
{
activeId
:
mockIssueWithEpic
.
id
,
boardItems
:
{
[
mockIssueWithEpic
.
id
]:
{
...
mockIssueWithEpic
}
},
epicsCacheById
:
{},
epicFetchInProgress
:
true
,
},
actionsMock
:
{
fetchEpicForActiveIssue
,
},
});
createWrapper
();
expect
(
fetchEpicForActiveIssue
).
toHaveBeenCalledTimes
(
1
);
});
it
(
'
flashes an error message when fetch fails
'
,
async
()
=>
{
createStore
({
initialState
:
{
activeId
:
mockIssueWithEpic
.
id
,
boardItems
:
{
[
mockIssueWithEpic
.
id
]:
{
...
mockIssueWithEpic
}
},
epicsCacheById
:
{},
epicFetchInProgress
:
true
,
},
actionsMock
:
{
fetchEpicForActiveIssue
:
jest
.
fn
().
mockRejectedValue
(
'
mayday
'
),
},
});
createWrapper
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
createFlash
).
toHaveBeenCalledTimes
(
1
);
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
wrapper
.
vm
.
$options
.
i18n
.
fetchEpicError
,
error
:
'
mayday
'
,
captureError
:
true
,
});
});
it
(
'
renders epic title when issue has an assigned epic
'
,
async
()
=>
{
createStore
({
initialState
:
{
activeId
:
mockIssueWithEpic
.
id
,
boardItems
:
{
[
mockIssueWithEpic
.
id
]:
{
...
mockIssueWithEpic
}
},
epicsCacheById
:
{
[
mockAssignedEpic
.
id
]:
{
...
mockAssignedEpic
}
},
epicFetchInProgress
:
false
,
},
});
createWrapper
();
await
wrapper
.
vm
.
$nextTick
();
expect
(
findEpicLink
().
isVisible
()).
toBe
(
true
);
expect
(
findEpicLink
().
text
()).
toBe
(
mockAssignedEpic
.
title
);
expect
(
findEpicLink
().
attributes
(
'
href
'
)).
toBe
(
mockAssignedEpic
.
webPath
);
});
});
describe
(
'
when epic is selected
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createStore
({
initialState
:
{
activeId
:
mockIssueWithoutEpic
.
id
,
boardItems
:
{
[
mockIssueWithoutEpic
.
id
]:
{
...
mockIssueWithoutEpic
}
},
epicsCacheById
:
{},
epicFetchInProgress
:
false
,
},
});
createWrapper
();
jest
.
spyOn
(
wrapper
.
vm
,
'
setActiveIssueEpic
'
).
mockImplementation
(
async
()
=>
{
// 'setActiveIssueEpic' sets the active issue's epic to the selected epic
// and stores the assigned epic's data in 'epicsCacheById'
store
.
state
.
epicFetchInProgress
=
true
;
store
.
state
.
boardItems
[
mockIssueWithoutEpic
.
id
].
epic
=
{
...
mockAssignedEpic
};
store
.
state
.
epicsCacheById
=
{
[
mockAssignedEpic
.
id
]:
{
...
mockAssignedEpic
}
};
store
.
state
.
epicFetchInProgress
=
false
;
});
findEpicSelect
().
vm
.
$emit
(
'
epicSelect
'
,
{
...
mockAssignedEpic
,
id
:
getIdFromGraphQLId
(
mockAssignedEpic
.
id
),
});
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
commits change to the server
'
,
()
=>
{
expect
(
wrapper
.
vm
.
setActiveIssueEpic
).
toHaveBeenCalledWith
(
mockAssignedEpic
.
id
);
expect
(
wrapper
.
vm
.
setActiveIssueEpic
).
toHaveBeenCalledTimes
(
1
);
});
it
(
'
collapses sidebar and renders epic title
'
,
()
=>
{
expect
(
findEpicLink
().
isVisible
()).
toBe
(
true
);
expect
(
findEpicLink
().
text
()).
toBe
(
mockAssignedEpic
.
title
);
expect
(
findEpicLink
().
attributes
(
'
href
'
)).
toBe
(
mockAssignedEpic
.
webPath
);
});
describe
(
'
when the selected epic did not change
'
,
()
=>
{
it
(
'
does not commit change to the server
'
,
async
()
=>
{
createStore
();
createWrapper
();
jest
.
spyOn
(
wrapper
.
vm
,
'
setActiveIssueEpic
'
).
mockImplementation
();
findEpicSelect
().
vm
.
$emit
(
'
epicSelect
'
,
null
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
wrapper
.
vm
.
setActiveIssueEpic
).
not
.
toHaveBeenCalled
();
});
});
});
describe
(
'
when no epic is selected
'
,
()
=>
{
beforeEach
(
async
()
=>
{
createStore
({
initialState
:
{
activeId
:
mockIssueWithEpic
.
id
,
boardItems
:
{
[
mockIssueWithEpic
.
id
]:
{
...
mockIssueWithEpic
}
},
epicsCacheById
:
{
[
mockAssignedEpic
.
id
]:
{
...
mockAssignedEpic
}
},
epicFetchInProgress
:
false
,
},
});
createWrapper
();
jest
.
spyOn
(
wrapper
.
vm
,
'
setActiveIssueEpic
'
).
mockImplementation
(
async
()
=>
{
// Remove assigned epic from the active issue
store
.
state
.
boardItems
[
mockIssueWithoutEpic
.
id
].
epic
=
null
;
});
findEpicSelect
().
vm
.
$emit
(
'
epicSelect
'
,
null
);
await
wrapper
.
vm
.
$nextTick
();
});
it
(
'
collapses sidebar and renders "None"
'
,
()
=>
{
expect
(
findCollapsed
().
isVisible
()).
toBe
(
true
);
expect
(
findCollapsed
().
text
()).
toBe
(
'
None
'
);
});
it
(
'
commits change to the server
'
,
()
=>
{
expect
(
wrapper
.
vm
.
setActiveIssueEpic
).
toHaveBeenCalledWith
(
null
);
expect
(
wrapper
.
vm
.
setActiveIssueEpic
).
toHaveBeenCalledTimes
(
1
);
});
});
it
(
'
flashes an error when update fails
'
,
async
()
=>
{
createStore
({
actionsMock
:
{
setActiveIssueEpic
:
jest
.
fn
().
mockRejectedValue
(
'
mayday
'
),
},
});
createWrapper
();
findEpicSelect
().
vm
.
$emit
(
'
epicSelect
'
,
{
id
:
'
foo
'
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
createFlash
).
toHaveBeenCalledTimes
(
1
);
expect
(
createFlash
).
toHaveBeenCalledWith
({
message
:
wrapper
.
vm
.
$options
.
i18n
.
updateEpicError
,
error
:
'
mayday
'
,
captureError
:
true
,
});
});
});
ee/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
0 → 100644
View file @
0e4094dd
This diff is collapsed.
Click to expand it.
ee/spec/frontend/sidebar/components/sidebar_iteration_widget_spec.js
View file @
0e4094dd
...
...
@@ -27,7 +27,7 @@ import {
mockIssue
,
mockGroupIterationsResponse
,
mockIteration2
,
mockMutationResponse
,
mock
Iteration
MutationResponse
,
emptyGroupIterationsResponse
,
noCurrentIterationResponse
,
}
from
'
../mock_data
'
;
...
...
@@ -379,7 +379,7 @@ describe('SidebarIterationWidget', () => {
describe
(
'
when dropdown is expanded and user can edit
'
,
()
=>
{
let
iterationMutationSpy
;
beforeEach
(
async
()
=>
{
iterationMutationSpy
=
jest
.
fn
().
mockResolvedValue
(
mockMutationResponse
);
iterationMutationSpy
=
jest
.
fn
().
mockResolvedValue
(
mock
Iteration
MutationResponse
);
await
createComponentWithApollo
({
requestHandlers
:
[[
projectIssueIterationMutation
,
iterationMutationSpy
]],
...
...
ee/spec/frontend/sidebar/mock_data.js
View file @
0e4094dd
...
...
@@ -25,6 +25,22 @@ export const mockIteration2 = {
state
:
'
opened
'
,
};
export
const
mockEpic1
=
{
__typename
:
'
Epic
'
,
id
:
'
gid://gitlab/Epic/1
'
,
title
:
'
Foobar Epic
'
,
webUrl
:
'
http://gdk.test:3000/groups/gitlab-org/-/epics/1
'
,
state
:
'
opened
'
,
};
export
const
mockEpic2
=
{
__typename
:
'
Epic
'
,
id
:
'
gid://gitlab/Epic/2
'
,
title
:
'
Awesome Epic
'
,
webUrl
:
'
http://gdk.test:3000/groups/gitlab-org/-/epics/2
'
,
state
:
'
opened
'
,
};
export
const
mockGroupIterationsResponse
=
{
data
:
{
workspace
:
{
...
...
@@ -37,6 +53,18 @@ export const mockGroupIterationsResponse = {
},
};
export
const
mockGroupEpicsResponse
=
{
data
:
{
workspace
:
{
attributes
:
{
nodes
:
[
mockEpic1
,
mockEpic2
],
},
__typename
:
'
EpicConnection
'
,
},
__typename
:
'
Group
'
,
},
};
export
const
emptyGroupIterationsResponse
=
{
data
:
{
workspace
:
{
...
...
@@ -49,6 +77,18 @@ export const emptyGroupIterationsResponse = {
},
};
export
const
emptyGroupEpicsResponse
=
{
data
:
{
workspace
:
{
attributes
:
{
nodes
:
[],
},
__typename
:
'
EpicConnection
'
,
},
__typename
:
'
Group
'
,
},
};
export
const
noCurrentIterationResponse
=
{
data
:
{
workspace
:
{
...
...
@@ -58,7 +98,16 @@ export const noCurrentIterationResponse = {
},
};
export
const
mockMutationResponse
=
{
export
const
noCurrentEpicResponse
=
{
data
:
{
workspace
:
{
issuable
:
{
id
:
mockIssueId
,
attribute
:
null
,
__typename
:
'
Issue
'
},
__typename
:
'
Project
'
,
},
},
};
export
const
mockIterationMutationResponse
=
{
data
:
{
issuableSetIteration
:
{
errors
:
[],
...
...
@@ -76,3 +125,22 @@ export const mockMutationResponse = {
},
},
};
export
const
mockEpicMutationResponse
=
{
data
:
{
issuableSetAttribute
:
{
errors
:
[],
issuable
:
{
id
:
'
gid://gitlab/Issue/1
'
,
attribute
:
{
id
:
'
gid://gitlab/Epic/2
'
,
title
:
'
Awesome Epic
'
,
state
:
'
opened
'
,
__typename
:
'
Epic
'
,
},
__typename
:
'
Issue
'
,
},
__typename
:
'
IssueSetEpicPayload
'
,
},
},
};
ee/spec/support/shared_examples/features/sidebar_shared_examples.rb
View file @
0e4094dd
...
...
@@ -2,6 +2,8 @@
RSpec
.
shared_examples
'issue boards sidebar EE'
do
context
'epics'
do
let
(
:epic_widget
)
{
find
(
'[data-testid="sidebar-epic"]'
)
}
context
'when epic feature available'
do
let_it_be
(
:epic1
)
{
create
(
:epic
,
group:
group
)
}
let_it_be
(
:epic2
)
{
create
(
:epic
,
group:
group
)
}
...
...
@@ -13,25 +15,25 @@ RSpec.shared_examples 'issue boards sidebar EE' do
stub_licensed_features
(
epics:
true
)
first_card_with_epic
.
click
wait_for_requests
end
it
'displays name of epic and links to it'
do
page
.
within
(
'[data-testid="sidebar-epic"]'
)
do
expect
(
page
).
to
have_link
(
epic1
.
title
,
href:
epic_path
(
epic1
))
within
(
epic_widget
)
do
expect
(
page
).
to
have_link
(
epic1
.
title
)
expect
(
find_link
(
epic1
.
title
)[
:href
]).
to
end_with
(
epic_path
(
epic1
))
end
end
it
'updates the epic associated with the issue'
do
page
.
within
(
'[data-testid="sidebar-epic"]'
)
do
find
(
"[data-testid='edit-button']"
).
click
within
(
epic_widget
)
do
click_button
'Edit'
wait_for_requests
find
(
'.gl-new-dropdown-item'
,
text:
epic2
.
title
).
click
wait_for_requests
expect
(
page
).
to
have_
link
(
epic2
.
title
,
href:
epic_path
(
epic2
)
)
expect
(
page
).
to
have_
content
(
epic2
.
title
)
end
end
...
...
locale/gitlab.pot
View file @
0e4094dd
...
...
@@ -11770,6 +11770,24 @@ msgstr ""
msgid "Drop your files to start your upload."
msgstr ""
msgid "DropdownWidget|An error occurred while fetching the assigned %{issuableAttribute} of the selected %{issuableType}."
msgstr ""
msgid "DropdownWidget|Assign %{issuableAttribute}"
msgstr ""
msgid "DropdownWidget|Failed to fetch the %{issuableAttribute} for this %{issuableType}. Please try again."
msgstr ""
msgid "DropdownWidget|Failed to set %{issuableAttribute} on this %{issuableType}. Please try again."
msgstr ""
msgid "DropdownWidget|No %{issuableAttribute}"
msgstr ""
msgid "DropdownWidget|No %{issuableAttribute} found"
msgstr ""
msgid "Due Date"
msgstr ""
...
...
@@ -18302,12 +18320,6 @@ msgstr ""
msgid "IssueAnalytics|Weight"
msgstr ""
msgid "IssueBoards|An error occurred while assigning the selected epic to the issue."
msgstr ""
msgid "IssueBoards|An error occurred while fetching the assigned epic of the selected issue."
msgstr ""
msgid "IssueBoards|An error occurred while setting notifications status. Please try again."
msgstr ""
...
...
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