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
3539abf3
Commit
3539abf3
authored
Dec 07, 2017
by
Clement Ho
Committed by
Jacob Schatz
Dec 07, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add epic information to issue sidebar
parent
02bd4f66
Changes
29
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
466 additions
and
108 deletions
+466
-108
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
...scripts/sidebar/components/assignees/sidebar_assignees.js
+15
-9
app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
.../sidebar/components/participants/sidebar_participants.vue
+8
-3
app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
...idebar/components/subscriptions/sidebar_subscriptions.vue
+6
-3
app/assets/javascripts/sidebar/mount_sidebar.js
app/assets/javascripts/sidebar/mount_sidebar.js
+56
-15
app/assets/javascripts/sidebar/sidebar_bundle.js
app/assets/javascripts/sidebar/sidebar_bundle.js
+3
-6
app/assets/javascripts/sidebar/sidebar_mediator.js
app/assets/javascripts/sidebar/sidebar_mediator.js
+0
-1
app/assets/javascripts/sidebar/stores/sidebar_store.js
app/assets/javascripts/sidebar/stores/sidebar_store.js
+27
-23
app/assets/stylesheets/framework/sidebar.scss
app/assets/stylesheets/framework/sidebar.scss
+5
-0
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+2
-1
app/serializers/issue_sidebar_entity.rb
app/serializers/issue_sidebar_entity.rb
+2
-0
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+2
-1
changelogs/unreleased-ee/epic-in-issue.yml
changelogs/unreleased-ee/epic-in-issue.yml
+5
-0
config/webpack.config.js
config/webpack.config.js
+1
-0
ee/app/assets/javascripts/sidebar/components/sidebar_item_epic.vue
...sets/javascripts/sidebar/components/sidebar_item_epic.vue
+84
-0
ee/app/assets/javascripts/sidebar/mount_sidebar.js
ee/app/assets/javascripts/sidebar/mount_sidebar.js
+17
-3
ee/app/assets/javascripts/sidebar/sidebar_bundle.js
ee/app/assets/javascripts/sidebar/sidebar_bundle.js
+14
-0
ee/app/assets/javascripts/sidebar/sidebar_mediator.js
ee/app/assets/javascripts/sidebar/sidebar_mediator.js
+1
-0
ee/app/assets/javascripts/sidebar/stores/sidebar_store.js
ee/app/assets/javascripts/sidebar/stores/sidebar_store.js
+11
-4
ee/app/serializers/ee/issue_sidebar_entity.rb
ee/app/serializers/ee/issue_sidebar_entity.rb
+9
-0
ee/app/serializers/epic_base_entity.rb
ee/app/serializers/epic_base_entity.rb
+9
-0
ee/app/views/shared/issuable/_sidebar_item_epic.haml
ee/app/views/shared/issuable/_sidebar_item_epic.haml
+8
-0
spec/ee/spec/features/issues/epic_in_issue_sidebar_spec.rb
spec/ee/spec/features/issues/epic_in_issue_sidebar_spec.rb
+34
-0
spec/fixtures/api/schemas/entities/issue_sidebar.json
spec/fixtures/api/schemas/entities/issue_sidebar.json
+8
-2
spec/javascripts/sidebar/ee_sidebar_item_epic_spec.js
spec/javascripts/sidebar/ee_sidebar_item_epic_spec.js
+85
-0
spec/javascripts/sidebar/ee_sidebar_mediator_spec.js
spec/javascripts/sidebar/ee_sidebar_mediator_spec.js
+4
-3
spec/javascripts/sidebar/ee_sidebar_store_spec.js
spec/javascripts/sidebar/ee_sidebar_store_spec.js
+15
-9
spec/javascripts/sidebar/sidebar_assignees_spec.js
spec/javascripts/sidebar/sidebar_assignees_spec.js
+24
-21
spec/javascripts/sidebar/sidebar_subscriptions_spec.js
spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+6
-3
spec/serializers/issue_serializer_spec.rb
spec/serializers/issue_serializer_spec.rb
+5
-1
No files found.
app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
View file @
3539abf3
import
Flash
from
'
../../../flash
'
;
import
AssigneeTitle
from
'
./assignee_title
'
;
import
Assignees
from
'
./assignees
'
;
import
Store
from
'
../../stores/sidebar_store
'
;
import
Mediator
from
'
../../sidebar_mediator
'
;
import
eventHub
from
'
../../event_hub
'
;
export
default
{
name
:
'
SidebarAssignees
'
,
data
()
{
return
{
mediator
:
new
Mediator
(),
store
:
new
Store
(),
loading
:
false
,
field
:
''
,
};
},
props
:
{
mediator
:
{
type
:
Object
,
required
:
true
,
},
field
:
{
type
:
String
,
required
:
true
,
},
signedIn
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
components
:
{
'
assignee-title
'
:
AssigneeTitle
,
assignees
:
Assignees
,
...
...
@@ -61,10 +71,6 @@ export default {
eventHub
.
$off
(
'
sidebar.removeAllAssignees
'
,
this
.
removeAllAssignees
);
eventHub
.
$off
(
'
sidebar.saveAssignees
'
,
this
.
saveAssignees
);
},
beforeMount
()
{
this
.
field
=
this
.
$el
.
dataset
.
field
;
this
.
signedIn
=
typeof
this
.
$el
.
dataset
.
signedIn
!==
'
undefined
'
;
},
template
:
`
<div>
<assignee-title
...
...
app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
View file @
3539abf3
<
script
>
import
Store
from
'
../../stores/sidebar_store
'
;
import
Mediator
from
'
../../sidebar_mediator
'
;
import
participants
from
'
./participants.vue
'
;
export
default
{
data
()
{
return
{
mediator
:
new
Mediator
(),
store
:
new
Store
(),
};
},
props
:
{
mediator
:
{
type
:
Object
,
required
:
true
,
},
},
components
:
{
participants
,
},
...
...
@@ -21,6 +25,7 @@ export default {
<participants
:loading=
"store.isFetching.participants"
:participants=
"store.participants"
:number-of-less-participants=
"7"
/>
:number-of-less-participants=
"7"
/>
</div>
</
template
>
app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
View file @
3539abf3
<
script
>
import
Store
from
'
../../stores/sidebar_store
'
;
import
Mediator
from
'
../../sidebar_mediator
'
;
import
eventHub
from
'
../../event_hub
'
;
import
Flash
from
'
../../../flash
'
;
import
{
__
}
from
'
../../../locale
'
;
...
...
@@ -9,11 +8,15 @@ import subscriptions from './subscriptions.vue';
export
default
{
data
()
{
return
{
mediator
:
new
Mediator
(),
store
:
new
Store
(),
};
},
props
:
{
mediator
:
{
type
:
Object
,
required
:
true
,
},
},
components
:
{
subscriptions
,
},
...
...
app/assets/javascripts/sidebar/mount_sidebar.js
View file @
3539abf3
...
...
@@ -10,6 +10,27 @@ import Translate from '../vue_shared/translate';
Vue
.
use
(
Translate
);
function
mountAssigneesComponent
(
mediator
)
{
const
el
=
document
.
getElementById
(
'
js-vue-sidebar-assignees
'
);
if
(
!
el
)
return
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
SidebarAssignees
,
},
render
:
createElement
=>
createElement
(
'
sidebar-assignees
'
,
{
props
:
{
mediator
,
field
:
el
.
dataset
.
field
,
signedIn
:
el
.
hasAttribute
(
'
data-signed-in
'
),
},
}),
});
}
function
mountConfidentialComponent
(
mediator
)
{
const
el
=
document
.
getElementById
(
'
js-confidential-entry-point
'
);
...
...
@@ -49,9 +70,10 @@ function mountLockComponent(mediator) {
}).
$mount
(
el
);
}
function
mountParticipantsComponent
()
{
function
mountParticipantsComponent
(
mediator
)
{
const
el
=
document
.
querySelector
(
'
.js-sidebar-participants-entry-point
'
);
// eslint-disable-next-line no-new
if
(
!
el
)
return
;
// eslint-disable-next-line no-new
...
...
@@ -60,11 +82,15 @@ function mountParticipantsComponent() {
components
:
{
sidebarParticipants
,
},
render
:
createElement
=>
createElement
(
'
sidebar-participants
'
,
{}),
render
:
createElement
=>
createElement
(
'
sidebar-participants
'
,
{
props
:
{
mediator
,
},
}),
});
}
function
mountSubscriptionsComponent
()
{
function
mountSubscriptionsComponent
(
mediator
)
{
const
el
=
document
.
querySelector
(
'
.js-sidebar-subscriptions-entry-point
'
);
if
(
!
el
)
return
;
...
...
@@ -75,22 +101,35 @@ function mountSubscriptionsComponent() {
components
:
{
sidebarSubscriptions
,
},
render
:
createElement
=>
createElement
(
'
sidebar-subscriptions
'
,
{}),
render
:
createElement
=>
createElement
(
'
sidebar-subscriptions
'
,
{
props
:
{
mediator
,
},
}),
});
}
function
mount
(
mediator
)
{
const
sidebarAssigneesEl
=
document
.
getElementById
(
'
js-vue-sidebar-assignees
'
);
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if
(
sidebarAssigneesEl
)
{
new
Vue
(
SidebarAssignees
).
$mount
(
sidebarAssigneesEl
);
}
function
mountTimeTrackingComponent
()
{
const
el
=
document
.
getElementById
(
'
issuable-time-tracker
'
);
if
(
!
el
)
return
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
SidebarTimeTracking
,
},
render
:
createElement
=>
createElement
(
'
sidebar-time-tracking
'
,
{}),
});
}
export
function
mountSidebar
(
mediator
)
{
mountAssigneesComponent
(
mediator
);
mountConfidentialComponent
(
mediator
);
mountLockComponent
(
mediator
);
mountParticipantsComponent
();
mountSubscriptionsComponent
();
mountParticipantsComponent
(
mediator
);
mountSubscriptionsComponent
(
mediator
);
new
SidebarMoveIssue
(
mediator
,
...
...
@@ -98,7 +137,9 @@ function mount(mediator) {
$
(
'
.js-move-issue-confirmation-button
'
),
).
init
();
new
Vue
(
SidebarTimeTracking
).
$mount
(
'
#issuable-time-tracker
'
);
mountTimeTrackingComponent
(
);
}
export
default
mount
;
export
function
getSidebarOptions
()
{
return
JSON
.
parse
(
document
.
querySelector
(
'
.js-sidebar-options
'
).
innerHTML
);
}
app/assets/javascripts/sidebar/sidebar_bundle.js
View file @
3539abf3
import
mountSidebarEE
from
'
ee/sidebar/mount_sidebar
'
;
import
Mediator
from
'
ee/sidebar/sidebar_mediator
'
;
import
mountSidebar
from
'
./mount_sidebar
'
;
import
Mediator
from
'
./sidebar_mediator
'
;
import
{
mountSidebar
,
getSidebarOptions
}
from
'
./mount_sidebar
'
;
function
domContentLoaded
()
{
const
sidebarOptions
=
JSON
.
parse
(
document
.
querySelector
(
'
.js-sidebar-options
'
).
innerHTML
);
const
mediator
=
new
Mediator
(
sidebarOptions
);
const
mediator
=
new
Mediator
(
getSidebarOptions
());
mediator
.
fetch
();
mountSidebar
(
mediator
);
mountSidebarEE
(
mediator
);
}
document
.
addEventListener
(
'
DOMContentLoaded
'
,
domContentLoaded
);
...
...
app/assets/javascripts/sidebar/sidebar_mediator.js
View file @
3539abf3
...
...
@@ -7,7 +7,6 @@ export default class SidebarMediator {
if
(
!
SidebarMediator
.
singleton
)
{
this
.
initSingleton
(
options
);
}
return
SidebarMediator
.
singleton
;
}
...
...
app/assets/javascripts/sidebar/stores/sidebar_store.js
View file @
3539abf3
export
default
class
SidebarStore
{
constructor
(
store
)
{
constructor
(
options
)
{
if
(
!
SidebarStore
.
singleton
)
{
const
{
currentUser
,
rootPath
,
editable
}
=
store
;
this
.
currentUser
=
currentUser
;
this
.
rootPath
=
rootPath
;
this
.
editable
=
editable
;
this
.
timeEstimate
=
0
;
this
.
totalTimeSpent
=
0
;
this
.
humanTimeEstimate
=
''
;
this
.
humanTimeSpent
=
''
;
this
.
assignees
=
[];
this
.
isFetching
=
{
assignees
:
true
,
participants
:
true
,
subscriptions
:
true
,
};
this
.
isLoading
=
{};
this
.
autocompleteProjects
=
[];
this
.
moveToProjectId
=
0
;
this
.
isLockDialogOpen
=
false
;
this
.
participants
=
[];
this
.
subscribed
=
null
;
SidebarStore
.
singleton
=
this
;
this
.
initSingleton
(
options
);
}
return
SidebarStore
.
singleton
;
}
initSingleton
(
options
)
{
const
{
currentUser
,
rootPath
,
editable
}
=
options
;
this
.
currentUser
=
currentUser
;
this
.
rootPath
=
rootPath
;
this
.
editable
=
editable
;
this
.
timeEstimate
=
0
;
this
.
totalTimeSpent
=
0
;
this
.
humanTimeEstimate
=
''
;
this
.
humanTimeSpent
=
''
;
this
.
assignees
=
[];
this
.
isFetching
=
{
assignees
:
true
,
participants
:
true
,
subscriptions
:
true
,
};
this
.
isLoading
=
{};
this
.
autocompleteProjects
=
[];
this
.
moveToProjectId
=
0
;
this
.
isLockDialogOpen
=
false
;
this
.
participants
=
[];
this
.
subscribed
=
null
;
SidebarStore
.
singleton
=
this
;
}
setAssigneeData
(
data
)
{
this
.
isFetching
.
assignees
=
false
;
if
(
data
.
assignees
)
{
...
...
app/assets/stylesheets/framework/sidebar.scss
View file @
3539abf3
...
...
@@ -50,6 +50,11 @@
&
:not
(
.disabled
)
{
cursor
:
pointer
;
}
svg
{
width
:
$gl-padding
;
height
:
$gl-padding
;
}
}
}
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
3539abf3
...
...
@@ -471,7 +471,8 @@
}
}
.milestone-title
span
{
.milestone-title
span
,
.collapse-truncated-title
{
@include
str-truncated
(
100%
);
display
:
block
;
margin
:
0
4px
;
...
...
app/serializers/issue_sidebar_entity.rb
View file @
3539abf3
class
IssueSidebarEntity
<
IssuableSidebarEntity
prepend
::
EE
::
IssueSidebarEntity
expose
:assignees
,
using:
API
::
Entities
::
UserBasic
end
app/views/shared/issuable/_sidebar.html.haml
View file @
3539abf3
-
todo
=
issuable_todo
(
issuable
)
-
content_for
:page_specific_javascripts
do
=
page_specific_javascript_bundle_tag
(
'common_vue'
)
=
page_specific_javascript_bundle_tag
(
'sidebar'
)
=
page_specific_javascript_bundle_tag
(
'
ee_
sidebar'
)
%aside
.right-sidebar.js-right-sidebar.js-issuable-sidebar
{
data:
{
signed:
{
in:
current_user
.
present?
}
},
class:
sidebar_gutter_collapsed_class
,
'aria-live'
=>
'polite'
}
.issuable-sidebar
{
data:
{
endpoint:
"#{issuable_json_path(issuable)}"
}
}
...
...
@@ -21,6 +21,7 @@
=
render
"shared/issuable/sidebar_todo"
,
todo:
todo
,
issuable:
issuable
,
is_collapsed:
true
.block.assignee
=
render
"shared/issuable/sidebar_assignees"
,
issuable:
issuable
,
can_edit_issuable:
can_edit_issuable
,
signed_in:
current_user
.
present?
=
render
"shared/issuable/sidebar_item_epic"
,
issuable:
issuable
.block.milestone
.sidebar-collapsed-icon
=
icon
(
'clock-o'
,
'aria-hidden'
:
'true'
)
...
...
changelogs/unreleased-ee/epic-in-issue.yml
0 → 100644
View file @
3539abf3
---
title
:
Add epic information to issue sidebar
merge_request
:
author
:
type
:
added
config/webpack.config.js
View file @
3539abf3
...
...
@@ -84,6 +84,7 @@ var config = {
registry_list
:
'
./registry/index.js
'
,
repo
:
'
./repo/index.js
'
,
sidebar
:
'
./sidebar/sidebar_bundle.js
'
,
ee_sidebar
:
'
ee/sidebar/sidebar_bundle.js
'
,
schedule_form
:
'
./pipeline_schedules/pipeline_schedule_form_bundle.js
'
,
schedules_index
:
'
./pipeline_schedules/pipeline_schedules_index_bundle.js
'
,
snippet
:
'
./snippet/snippet_bundle.js
'
,
...
...
ee/app/assets/javascripts/sidebar/components/sidebar_item_epic.vue
0 → 100644
View file @
3539abf3
<
script
>
import
LoadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
{
spriteIcon
}
from
'
~/lib/utils/common_utils
'
;
import
Store
from
'
../stores/sidebar_store
'
;
export
default
{
name
:
'
sidebarItemEpic
'
,
data
()
{
return
{
store
:
new
Store
(),
};
},
components
:
{
LoadingIcon
,
},
directives
:
{
tooltip
,
},
computed
:
{
isLoading
()
{
return
this
.
store
.
isFetching
.
epic
;
},
epicIcon
()
{
return
spriteIcon
(
'
epic
'
);
},
epicUrl
()
{
return
this
.
store
.
epic
.
url
;
},
epicTitle
()
{
return
this
.
store
.
epic
.
title
;
},
hasEpic
()
{
return
this
.
epicUrl
&&
this
.
epicTitle
;
},
collapsedTitle
()
{
return
this
.
hasEpic
?
this
.
epicTitle
:
'
None
'
;
},
},
};
</
script
>
<
template
>
<div>
<div
class=
"sidebar-collapsed-icon"
>
<div
v-html=
"epicIcon"
></div>
<span
v-if=
"!isLoading"
class=
"collapse-truncated-title"
:title=
"epicTitle"
data-container=
"body"
data-placement=
"left"
v-tooltip
>
{{
collapsedTitle
}}
</span>
</div>
<div
class=
"title hide-collapsed"
>
Epic
<loading-icon
v-if=
"isLoading"
:inline=
"true"
/>
</div>
<div
v-if=
"!isLoading"
class=
"value hide-collapsed"
>
<a
v-if=
"hasEpic"
class=
"bold"
:href=
"epicUrl"
>
{{
epicTitle
}}
</a>
<span
v-else
class=
"no-value"
>
None
</span>
</div>
</div>
</
template
>
ee/app/assets/javascripts/sidebar/mount_sidebar.js
View file @
3539abf3
import
Vue
from
'
vue
'
;
import
*
as
CEMountSidebar
from
'
~/sidebar/mount_sidebar
'
;
import
sidebarWeight
from
'
./components/weight/sidebar_weight.vue
'
;
import
SidebarItemEpic
from
'
./components/sidebar_item_epic.vue
'
;
function
mountWeightComponent
(
mediator
)
{
const
el
=
document
.
querySelector
(
'
.js-sidebar-weight-entry-point
'
);
...
...
@@ -20,8 +22,20 @@ function mountWeightComponent(mediator) {
});
}
function
mount
(
mediator
)
{
mountWeightComponent
(
mediator
);
function
mountEpic
()
{
const
el
=
document
.
querySelector
(
'
#js-vue-sidebar-item-epic
'
);
return
new
Vue
({
el
,
components
:
{
SidebarItemEpic
,
},
render
:
createElement
=>
createElement
(
'
sidebar-item-epic
'
,
{}),
});
}
export
default
mount
;
export
default
function
mountSidebar
(
mediator
)
{
CEMountSidebar
.
mountSidebar
(
mediator
);
mountWeightComponent
(
mediator
);
mountEpic
();
}
ee/app/assets/javascripts/sidebar/sidebar_bundle.js
0 → 100644
View file @
3539abf3
import
{
getSidebarOptions
}
from
'
~/sidebar/mount_sidebar
'
;
import
Mediator
from
'
./sidebar_mediator
'
;
import
mountSidebar
from
'
./mount_sidebar
'
;
function
domContentLoaded
()
{
const
mediator
=
new
Mediator
(
getSidebarOptions
());
mediator
.
fetch
();
mountSidebar
(
mediator
);
}
document
.
addEventListener
(
'
DOMContentLoaded
'
,
domContentLoaded
);
export
default
domContentLoaded
;
ee/app/assets/javascripts/sidebar/sidebar_mediator.js
View file @
3539abf3
...
...
@@ -10,6 +10,7 @@ export default class SidebarMediator extends CESidebarMediator {
processFetchedData
(
data
)
{
super
.
processFetchedData
(
data
);
this
.
store
.
setWeightData
(
data
);
this
.
store
.
setEpicData
(
data
);
}
updateWeight
(
newWeight
)
{
...
...
ee/app/assets/javascripts/sidebar/stores/sidebar_store.js
View file @
3539abf3
import
CESidebarStore
from
'
~/sidebar/stores/sidebar_store
'
;
export
default
class
SidebarStore
extends
CESidebarStore
{
constructor
(
store
)
{
super
(
store
);
initSingleton
(
options
)
{
super
.
initSingleton
(
options
);
this
.
isFetching
.
weight
=
true
;
this
.
isFetching
.
epic
=
true
;
this
.
isLoading
.
weight
=
false
;
this
.
weight
=
null
;
this
.
weightOptions
=
store
.
weightOptions
;
this
.
weightNoneValue
=
store
.
weightNoneValue
;
this
.
weightOptions
=
options
.
weightOptions
;
this
.
weightNoneValue
=
options
.
weightNoneValue
;
this
.
epic
=
{};
}
setWeightData
(
data
)
{
...
...
@@ -19,4 +21,9 @@ export default class SidebarStore extends CESidebarStore {
setWeight
(
newWeight
)
{
this
.
weight
=
newWeight
;
}
setEpicData
(
data
)
{
this
.
isFetching
.
epic
=
false
;
this
.
epic
=
data
.
epic
||
{};
}
}
ee/app/serializers/ee/issue_sidebar_entity.rb
0 → 100644
View file @
3539abf3
module
EE
module
IssueSidebarEntity
extend
ActiveSupport
::
Concern
prepended
do
expose
:epic
,
using:
EpicBaseEntity
end
end
end
ee/app/serializers/epic_base_entity.rb
0 → 100644
View file @
3539abf3
class
EpicBaseEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:id
expose
:title
expose
:url
do
|
epic
|
group_epic_path
(
epic
.
group
,
epic
)
end
end
ee/app/views/shared/issuable/_sidebar_item_epic.haml
0 → 100644
View file @
3539abf3
-
return
unless
issuable
.
project
.
group
&
.
feature_available?
(
:epics
)
-
if
issuable
.
is_a?
(
Issue
)
.block.epic
#js-vue-sidebar-item-epic
.title.hide-collapsed
Epic
=
icon
(
'spinner spin'
)
spec/ee/spec/features/issues/epic_in_issue_sidebar_spec.rb
0 → 100644
View file @
3539abf3
require
'spec_helper'
describe
'Epic in issue sidebar'
,
:js
do
let
(
:user
)
{
create
(
:user
)
}
let
(
:group
)
{
create
(
:group
,
:public
)
}
let
(
:epic
)
{
create
(
:epic
,
group:
group
)
}
let
(
:project
)
{
create
(
:project
,
:public
,
group:
group
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let!
(
:epic_issue
)
{
create
(
:epic_issue
,
epic:
epic
,
issue:
issue
)
}
context
'when epics available'
do
before
do
stub_licensed_features
(
epics:
true
)
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
end
it
'shows epic in issue sidebar'
do
expect
(
page
.
find
(
'.block.epic .value'
)).
to
have_content
(
epic
.
title
)
end
end
context
'when epics unavailable'
do
before
do
stub_licensed_features
(
epics:
false
)
sign_in
(
user
)
visit
project_issue_path
(
project
,
issue
)
end
it
'does not show epic in issue sidebar'
do
expect
(
page
).
not_to
have_selector
(
'.block.epic'
)
end
end
end
spec/fixtures/api/schemas/entities/issue_sidebar.json
View file @
3539abf3
{
"type"
:
"object"
,
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"iid"
:
{
"type"
:
"integer"
},
"subscribed"
:
{
"type"
:
"boolean"
},
"time_estimate"
:
{
"type"
:
"integer"
},
"total_time_spent"
:
{
"type"
:
"integer"
},
...
...
@@ -16,6 +14,14 @@
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"../public_api/v4/user/basic.json"
}
},
"epic"
:
{
"type"
:
[
"object"
,
"null"
],
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"title"
:
{
"type"
:
"string"
},
"url"
:
{
"type"
:
"string"
}
}
},
"weight"
:
{
"type"
:
[
"integer"
,
"null"
]
}
},
"additionalProperties"
:
false
...
...
spec/javascripts/sidebar/ee_sidebar_item_epic_spec.js
0 → 100644
View file @
3539abf3
import
Vue
from
'
vue
'
;
import
CESidebarStore
from
'
~/sidebar/stores/sidebar_store
'
;
import
SidebarStore
from
'
ee/sidebar/stores/sidebar_store
'
;
import
sidebarItemEpic
from
'
ee/sidebar/components/sidebar_item_epic.vue
'
;
import
mountComponent
from
'
../helpers/vue_mount_component_helper
'
;
describe
(
'
sidebarItemEpic
'
,
()
=>
{
let
vm
;
let
sidebarStore
;
beforeEach
(()
=>
{
sidebarStore
=
new
SidebarStore
({
currentUser
:
''
,
rootPath
:
''
,
editable
:
false
,
});
const
SidebarItemEpic
=
Vue
.
extend
(
sidebarItemEpic
);
vm
=
mountComponent
(
SidebarItemEpic
,
{});
});
afterEach
(()
=>
{
vm
.
$destroy
();
CESidebarStore
.
singleton
=
null
;
});
describe
(
'
loading
'
,
()
=>
{
it
(
'
shows loading icon
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.fa-spin
'
)).
toBeDefined
();
});
it
(
'
hides collapsed title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.sidebar-collapsed-icon .collapsed-truncated-title
'
)).
toBeNull
();
});
});
describe
(
'
loaded
'
,
()
=>
{
const
epicTitle
=
'
epic title
'
;
const
url
=
'
https://gitlab.com/
'
;
beforeEach
((
done
)
=>
{
sidebarStore
.
setEpicData
({
epic
:
{
title
:
epicTitle
,
id
:
1
,
url
,
},
});
Vue
.
nextTick
(
done
);
});
it
(
'
shows epic title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.value
'
).
innerText
.
trim
()).
toEqual
(
epicTitle
);
});
it
(
'
links epic title to epic url
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
a
'
).
href
).
toEqual
(
url
);
});
it
(
'
shows epic title as collapsed title tooltip
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.collapse-truncated-title
'
).
getAttribute
(
'
title
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.collapse-truncated-title
'
).
getAttribute
(
'
data-original-title
'
)).
toEqual
(
epicTitle
);
});
describe
(
'
no epic
'
,
()
=>
{
beforeEach
((
done
)
=>
{
sidebarStore
.
epic
=
{};
Vue
.
nextTick
(
done
);
});
it
(
'
shows none as the epic text
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.value
'
).
innerText
.
trim
()).
toEqual
(
'
None
'
);
});
it
(
'
shows none as the collapsed title
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.collapse-truncated-title
'
).
innerText
.
trim
()).
toEqual
(
'
None
'
);
});
it
(
'
hides collapsed title tooltip
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.collapse-truncated-title
'
).
getAttribute
(
'
title
'
)).
toBeNull
();
});
});
});
});
spec/javascripts/sidebar/ee_sidebar_mediator_spec.js
View file @
3539abf3
import
Vue
from
'
vue
'
;
import
SidebarMediator
from
'
ee/sidebar/sidebar_mediator
'
;
import
SidebarStore
from
'
ee/sidebar/stores/sidebar_store
'
;
import
CESidebarMediator
from
'
~/sidebar/sidebar_mediator
'
;
import
CESidebarStore
from
'
~/sidebar/stores/sidebar_store
'
;
import
SidebarService
from
'
~/sidebar/services/sidebar_service
'
;
import
Mock
from
'
./ee_mock_data
'
;
...
...
@@ -12,8 +13,8 @@ describe('EE Sidebar mediator', () => {
afterEach
(()
=>
{
SidebarService
.
singleton
=
null
;
SidebarStore
.
singleton
=
null
;
SidebarMediator
.
singleton
=
null
;
CE
SidebarStore
.
singleton
=
null
;
CE
SidebarMediator
.
singleton
=
null
;
Vue
.
http
.
interceptors
=
_
.
without
(
Vue
.
http
.
interceptors
,
Mock
.
sidebarMockInterceptor
);
});
...
...
spec/javascripts/sidebar/ee_sidebar_store_spec.js
View file @
3539abf3
import
SidebarStore
from
'
ee/sidebar/stores/sidebar_store
'
;
import
CESidebarStore
from
'
~/sidebar/stores/sidebar_store
'
;
describe
(
'
EE Sidebar store
'
,
()
=>
{
let
store
;
beforeEach
(()
=>
{
this
.
store
=
new
SidebarStore
({
store
=
new
SidebarStore
({
weight
:
null
,
weightOptions
:
[
'
No Weight
'
,
0
,
1
,
3
],
weightNoneValue
:
'
No Weight
'
,
});
});
afterEach
(()
=>
{
SidebarStore
.
singleton
=
null
;
// Since CESidebarStore stores the actual singleton instance
// we need to clear that specific reference
CESidebarStore
.
singleton
=
null
;
});
it
(
'
sets weight data
'
,
()
=>
{
expect
(
this
.
store
.
weight
).
toEqual
(
null
);
expect
(
store
.
weight
).
toEqual
(
null
);
const
weight
=
3
;
this
.
store
.
setWeightData
({
store
.
setWeightData
({
weight
,
});
expect
(
this
.
store
.
isFetching
.
weight
).
toEqual
(
false
);
expect
(
this
.
store
.
weight
).
toEqual
(
weight
);
expect
(
store
.
isFetching
.
weight
).
toEqual
(
false
);
expect
(
store
.
weight
).
toEqual
(
weight
);
});
it
(
'
set weight
'
,
()
=>
{
const
weight
=
3
;
this
.
store
.
setWeight
(
weight
);
expect
(
store
.
weight
).
toEqual
(
null
);
const
weight
=
1
;
store
.
setWeight
(
weight
);
expect
(
this
.
store
.
weight
).
toEqual
(
weight
);
expect
(
store
.
weight
).
toEqual
(
weight
);
});
});
spec/javascripts/sidebar/sidebar_assignees_spec.js
View file @
3539abf3
...
...
@@ -4,20 +4,29 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import
SidebarService
from
'
~/sidebar/services/sidebar_service
'
;
import
SidebarStore
from
'
~/sidebar/stores/sidebar_store
'
;
import
Mock
from
'
./mock_data
'
;
import
mountComponent
from
'
../helpers/vue_mount_component_helper
'
;
describe
(
'
sidebar assignees
'
,
()
=>
{
let
component
;
let
SidebarAssigneeComponent
;
let
vm
;
let
mediator
;
let
sidebarAssigneesEl
;
preloadFixtures
(
'
issues/open-issue.html.raw
'
);
beforeEach
(()
=>
{
Vue
.
http
.
interceptors
.
push
(
Mock
.
sidebarMockInterceptor
);
SidebarAssigneeComponent
=
Vue
.
extend
(
SidebarAssignees
);
spyOn
(
SidebarMediator
.
prototype
,
'
saveAssignees
'
).
and
.
callThrough
();
spyOn
(
SidebarMediator
.
prototype
,
'
assignYourself
'
).
and
.
callThrough
();
this
.
mediator
=
new
SidebarMediator
(
Mock
.
mediator
);
loadFixtures
(
'
issues/open-issue.html.raw
'
);
this
.
sidebarAssigneesEl
=
document
.
querySelector
(
'
#js-vue-sidebar-assignees
'
);
mediator
=
new
SidebarMediator
(
Mock
.
mediator
);
spyOn
(
mediator
,
'
saveAssignees
'
).
and
.
callThrough
();
spyOn
(
mediator
,
'
assignYourself
'
).
and
.
callThrough
();
const
SidebarAssigneeComponent
=
Vue
.
extend
(
SidebarAssignees
);
sidebarAssigneesEl
=
document
.
querySelector
(
'
#js-vue-sidebar-assignees
'
);
vm
=
mountComponent
(
SidebarAssigneeComponent
,
{
mediator
,
field
:
sidebarAssigneesEl
.
dataset
.
field
,
},
sidebarAssigneesEl
);
});
afterEach
(()
=>
{
...
...
@@ -28,30 +37,24 @@ describe('sidebar assignees', () => {
});
it
(
'
calls the mediator when saves the assignees
'
,
()
=>
{
component
=
new
SidebarAssigneeComponent
()
.
$mount
(
this
.
sidebarAssigneesEl
);
component
.
saveAssignees
();
expect
(
SidebarMediator
.
prototype
.
saveAssignees
).
toHaveBeenCalled
();
vm
.
saveAssignees
();
expect
(
mediator
.
saveAssignees
).
toHaveBeenCalled
();
});
it
(
'
calls the mediator when "assignSelf" method is called
'
,
()
=>
{
component
=
new
SidebarAssigneeComponent
()
.
$mount
(
this
.
sidebarAssigneesEl
);
component
.
assignSelf
();
vm
.
assignSelf
();
expect
(
SidebarMediator
.
prototype
.
assignYourself
).
toHaveBeenCalled
();
expect
(
this
.
mediator
.
store
.
assignees
.
length
).
toEqual
(
1
);
expect
(
mediator
.
assignYourself
).
toHaveBeenCalled
();
expect
(
mediator
.
store
.
assignees
.
length
).
toEqual
(
1
);
});
it
(
'
hides assignees until fetched
'
,
(
done
)
=>
{
component
=
new
SidebarAssigneeComponent
().
$mount
(
this
.
sidebarAssigneesEl
);
const
currentAssignee
=
this
.
sidebarAssigneesEl
.
querySelector
(
'
.value
'
);
const
currentAssignee
=
sidebarAssigneesEl
.
querySelector
(
'
.value
'
);
expect
(
currentAssignee
).
toBe
(
null
);
component
.
store
.
isFetching
.
assignees
=
false
;
vm
.
store
.
isFetching
.
assignees
=
false
;
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.value
'
)).
toBeVisible
();
expect
(
vm
.
$el
.
querySelector
(
'
.value
'
)).
toBeVisible
();
done
();
});
});
...
...
spec/javascripts/sidebar/sidebar_subscriptions_spec.js
View file @
3539abf3
...
...
@@ -26,11 +26,14 @@ describe('Sidebar Subscriptions', function () {
});
it
(
'
calls the mediator toggleSubscription on event
'
,
()
=>
{
spyOn
(
SidebarMediator
.
prototype
,
'
toggleSubscription
'
).
and
.
returnValue
(
Promise
.
resolve
());
vm
=
mountComponent
(
SidebarSubscriptions
,
{});
const
mediator
=
new
SidebarMediator
();
spyOn
(
mediator
,
'
toggleSubscription
'
).
and
.
returnValue
(
Promise
.
resolve
());
vm
=
mountComponent
(
SidebarSubscriptions
,
{
mediator
,
});
eventHub
.
$emit
(
'
toggleSubscription
'
);
expect
(
SidebarMediator
.
prototype
.
toggleSubscription
).
toHaveBeenCalled
();
expect
(
mediator
.
toggleSubscription
).
toHaveBeenCalled
();
});
});
spec/serializers/issue_serializer_spec.rb
View file @
3539abf3
...
...
@@ -20,8 +20,12 @@ describe IssueSerializer do
context
'sidebar issue serialization'
do
let
(
:serializer
)
{
'sidebar'
}
before
do
create
(
:epic_issue
,
issue:
resource
)
end
it
'matches sidebar issue json schema'
do
expect
(
json_entity
).
to
match_schema
(
'entities/issue_sidebar'
)
expect
(
json_entity
).
to
match_schema
(
'entities/issue_sidebar'
,
strict:
true
)
end
end
end
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