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
6dc9028f
Commit
6dc9028f
authored
Oct 31, 2017
by
Eric Eastwood
Committed by
Phil Hughes
Oct 31, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Load participants async
parent
74a0e855
Changes
45
Hide whitespace changes
Inline
Side-by-side
Showing
45 changed files
with
911 additions
and
205 deletions
+911
-205
app/assets/javascripts/issuable_context.js
app/assets/javascripts/issuable_context.js
+0
-28
app/assets/javascripts/sidebar/components/participants/participants.vue
...ascripts/sidebar/components/participants/participants.vue
+125
-0
app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
.../sidebar/components/participants/sidebar_participants.vue
+26
-0
app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
...idebar/components/subscriptions/sidebar_subscriptions.vue
+45
-0
app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
...cripts/sidebar/components/subscriptions/subscriptions.vue
+60
-0
app/assets/javascripts/sidebar/services/sidebar_service.js
app/assets/javascripts/sidebar/services/sidebar_service.js
+5
-0
app/assets/javascripts/sidebar/sidebar_bundle.js
app/assets/javascripts/sidebar/sidebar_bundle.js
+34
-0
app/assets/javascripts/sidebar/sidebar_mediator.js
app/assets/javascripts/sidebar/sidebar_mediator.js
+16
-0
app/assets/javascripts/sidebar/stores/sidebar_store.js
app/assets/javascripts/sidebar/stores/sidebar_store.js
+22
-0
app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
...ts/vue_merge_request_widget/services/mr_widget_service.js
+1
-1
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+4
-2
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+1
-1
app/controllers/projects/merge_requests_controller.rb
app/controllers/projects/merge_requests_controller.rb
+1
-1
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+13
-10
app/models/concerns/subscribable.rb
app/models/concerns/subscribable.rb
+2
-0
app/serializers/issuable_sidebar_entity.rb
app/serializers/issuable_sidebar_entity.rb
+16
-0
app/serializers/issue_serializer.rb
app/serializers/issue_serializer.rb
+14
-1
app/serializers/issue_sidebar_entity.rb
app/serializers/issue_sidebar_entity.rb
+3
-0
app/serializers/merge_request_basic_entity.rb
app/serializers/merge_request_basic_entity.rb
+1
-5
app/serializers/merge_request_serializer.rb
app/serializers/merge_request_serializer.rb
+8
-1
app/views/shared/issuable/_participants.html.haml
app/views/shared/issuable/_participants.html.haml
+0
-18
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+3
-10
changelogs/unreleased/23206-load-participants-async.yml
changelogs/unreleased/23206-load-participants-async.yml
+5
-0
features/steps/project/issues/issues.rb
features/steps/project/issues/issues.rb
+4
-2
spec/controllers/projects/merge_requests_controller_spec.rb
spec/controllers/projects/merge_requests_controller_spec.rb
+3
-3
spec/features/boards/boards_spec.rb
spec/features/boards/boards_spec.rb
+1
-1
spec/features/projects/merge_requests/user_manages_subscription_spec.rb
...projects/merge_requests/user_manages_subscription_spec.rb
+1
-1
spec/fixtures/api/schemas/entities/issue.json
spec/fixtures/api/schemas/entities/issue.json
+44
-0
spec/fixtures/api/schemas/entities/issue_sidebar.json
spec/fixtures/api/schemas/entities/issue_sidebar.json
+21
-0
spec/fixtures/api/schemas/entities/label.json
spec/fixtures/api/schemas/entities/label.json
+26
-0
spec/fixtures/api/schemas/entities/merge_request_basic.json
spec/fixtures/api/schemas/entities/merge_request_basic.json
+3
-1
spec/fixtures/api/schemas/issue.json
spec/fixtures/api/schemas/issue.json
+1
-26
spec/javascripts/issuable_context_spec.js
spec/javascripts/issuable_context_spec.js
+0
-33
spec/javascripts/sidebar/mock_data.js
spec/javascripts/sidebar/mock_data.js
+2
-0
spec/javascripts/sidebar/participants_spec.js
spec/javascripts/sidebar/participants_spec.js
+174
-0
spec/javascripts/sidebar/sidebar_mediator_spec.js
spec/javascripts/sidebar/sidebar_mediator_spec.js
+15
-2
spec/javascripts/sidebar/sidebar_service_spec.js
spec/javascripts/sidebar/sidebar_service_spec.js
+14
-3
spec/javascripts/sidebar/sidebar_store_spec.js
spec/javascripts/sidebar/sidebar_store_spec.js
+73
-20
spec/javascripts/sidebar/sidebar_subscriptions_spec.js
spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+36
-0
spec/javascripts/sidebar/subscriptions_spec.js
spec/javascripts/sidebar/subscriptions_spec.js
+42
-0
spec/models/concerns/subscribable_spec.rb
spec/models/concerns/subscribable_spec.rb
+6
-0
spec/serializers/issue_serializer_spec.rb
spec/serializers/issue_serializer_spec.rb
+27
-0
spec/serializers/merge_request_basic_serializer_spec.rb
spec/serializers/merge_request_basic_serializer_spec.rb
+7
-3
spec/serializers/merge_request_serializer_spec.rb
spec/serializers/merge_request_serializer_spec.rb
+6
-6
spec/views/shared/issuable/_participants.html.haml.rb
spec/views/shared/issuable/_participants.html.haml.rb
+0
-26
No files found.
app/assets/javascripts/issuable_context.js
View file @
6dc9028f
...
...
@@ -2,11 +2,8 @@ import Cookies from 'js-cookie';
import
bp
from
'
./breakpoints
'
;
import
UsersSelect
from
'
./users_select
'
;
const
PARTICIPANTS_ROW_COUNT
=
7
;
export
default
class
IssuableContext
{
constructor
(
currentUser
)
{
this
.
initParticipants
();
this
.
userSelect
=
new
UsersSelect
(
currentUser
);
$
(
'
select.select2
'
).
select2
({
...
...
@@ -51,29 +48,4 @@ export default class IssuableContext {
}
});
}
initParticipants
()
{
$
(
document
).
on
(
'
click
'
,
'
.js-participants-more
'
,
this
.
toggleHiddenParticipants
);
return
$
(
'
.js-participants-author
'
).
each
(
function
forEachAuthor
(
i
)
{
if
(
i
>=
PARTICIPANTS_ROW_COUNT
)
{
$
(
this
).
addClass
(
'
js-participants-hidden
'
).
hide
();
}
});
}
toggleHiddenParticipants
()
{
const
currentText
=
$
(
this
).
text
().
trim
();
const
lessText
=
$
(
this
).
data
(
'
less-text
'
);
const
originalText
=
$
(
this
).
data
(
'
original-text
'
);
if
(
currentText
===
originalText
)
{
$
(
this
).
text
(
lessText
);
if
(
gl
.
lazyLoader
)
gl
.
lazyLoader
.
loadCheck
();
}
else
{
$
(
this
).
text
(
originalText
);
}
$
(
'
.js-participants-hidden
'
).
toggle
();
}
}
app/assets/javascripts/sidebar/components/participants/participants.vue
0 → 100644
View file @
6dc9028f
<
script
>
import
{
__
,
n__
,
sprintf
}
from
'
../../../locale
'
;
import
loadingIcon
from
'
../../../vue_shared/components/loading_icon.vue
'
;
import
userAvatarImage
from
'
../../../vue_shared/components/user_avatar/user_avatar_image.vue
'
;
export
default
{
props
:
{
loading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
participants
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
[],
},
numberOfLessParticipants
:
{
type
:
Number
,
required
:
false
,
default
:
7
,
},
},
data
()
{
return
{
isShowingMoreParticipants
:
false
,
};
},
components
:
{
loadingIcon
,
userAvatarImage
,
},
computed
:
{
lessParticipants
()
{
return
this
.
participants
.
slice
(
0
,
this
.
numberOfLessParticipants
);
},
visibleParticipants
()
{
return
this
.
isShowingMoreParticipants
?
this
.
participants
:
this
.
lessParticipants
;
},
hasMoreParticipants
()
{
return
this
.
participants
.
length
>
this
.
numberOfLessParticipants
;
},
toggleLabel
()
{
let
label
=
''
;
if
(
this
.
isShowingMoreParticipants
)
{
label
=
__
(
'
- show less
'
);
}
else
{
label
=
sprintf
(
__
(
'
+ %{moreCount} more
'
),
{
moreCount
:
this
.
participants
.
length
-
this
.
numberOfLessParticipants
,
});
}
return
label
;
},
participantLabel
()
{
return
sprintf
(
n__
(
'
%{count} participant
'
,
'
%{count} participants
'
,
this
.
participants
.
length
),
{
count
:
this
.
loading
?
''
:
this
.
participantCount
},
);
},
participantCount
()
{
return
this
.
participants
.
length
;
},
},
methods
:
{
toggleMoreParticipants
()
{
this
.
isShowingMoreParticipants
=
!
this
.
isShowingMoreParticipants
;
},
},
};
</
script
>
<
template
>
<div>
<div
class=
"sidebar-collapsed-icon"
>
<i
class=
"fa fa-users"
aria-hidden=
"true"
>
</i>
<loading-icon
v-if=
"loading"
class=
"js-participants-collapsed-loading-icon"
/>
<span
v-else
class=
"js-participants-collapsed-count"
>
{{
participantCount
}}
</span>
</div>
<div
class=
"title hide-collapsed"
>
<loading-icon
v-if=
"loading"
:inline=
"true"
class=
"js-participants-expanded-loading-icon"
/>
{{
participantLabel
}}
</div>
<div
class=
"participants-list hide-collapsed"
>
<div
v-for=
"participant in visibleParticipants"
:key=
"participant.id"
class=
"participants-author js-participants-author"
>
<a
class=
"author_link"
:href=
"participant.web_url"
>
<user-avatar-image
:lazy=
"true"
:img-src=
"participant.avatar_url"
css-classes=
"avatar-inline"
:size=
"24"
:tooltip-text=
"participant.name"
tooltip-placement=
"bottom"
/>
</a>
</div>
</div>
<div
v-if=
"hasMoreParticipants"
class=
"participants-more hide-collapsed"
>
<button
type=
"button"
class=
"btn-transparent btn-blank js-toggle-participants-button"
@
click=
"toggleMoreParticipants"
>
{{
toggleLabel
}}
</button>
</div>
</div>
</
template
>
app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue
0 → 100644
View file @
6dc9028f
<
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
(),
};
},
components
:
{
participants
,
},
};
</
script
>
<
template
>
<div
class=
"block participants"
>
<participants
:loading=
"store.isFetching.participants"
:participants=
"store.participants"
:number-of-less-participants=
"7"
/>
</div>
</
template
>
app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue
0 → 100644
View file @
6dc9028f
<
script
>
import
Store
from
'
../../stores/sidebar_store
'
;
import
Mediator
from
'
../../sidebar_mediator
'
;
import
eventHub
from
'
../../event_hub
'
;
import
Flash
from
'
../../../flash
'
;
import
subscriptions
from
'
./subscriptions.vue
'
;
export
default
{
data
()
{
return
{
mediator
:
new
Mediator
(),
store
:
new
Store
(),
};
},
components
:
{
subscriptions
,
},
methods
:
{
onToggleSubscription
()
{
this
.
mediator
.
toggleSubscription
()
.
catch
(()
=>
{
Flash
(
'
Error occurred when toggling the notification subscription
'
);
});
},
},
created
()
{
eventHub
.
$on
(
'
toggleSubscription
'
,
this
.
onToggleSubscription
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'
toggleSubscription
'
,
this
.
onToggleSubscription
);
},
};
</
script
>
<
template
>
<div
class=
"block subscriptions"
>
<subscriptions
:loading=
"store.isFetching.subscriptions"
:subscribed=
"store.subscribed"
/>
</div>
</
template
>
app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue
0 → 100644
View file @
6dc9028f
<
script
>
import
{
__
}
from
'
../../../locale
'
;
import
eventHub
from
'
../../event_hub
'
;
import
loadingButton
from
'
../../../vue_shared/components/loading_button.vue
'
;
export
default
{
props
:
{
loading
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
subscribed
:
{
type
:
Boolean
,
required
:
false
,
},
},
components
:
{
loadingButton
,
},
computed
:
{
buttonLabel
()
{
let
label
;
if
(
this
.
subscribed
===
false
)
{
label
=
__
(
'
Subscribe
'
);
}
else
if
(
this
.
subscribed
===
true
)
{
label
=
__
(
'
Unsubscribe
'
);
}
return
label
;
},
},
methods
:
{
toggleSubscription
()
{
eventHub
.
$emit
(
'
toggleSubscription
'
);
},
},
};
</
script
>
<
template
>
<div>
<div
class=
"sidebar-collapsed-icon"
>
<i
class=
"fa fa-rss"
aria-hidden=
"true"
>
</i>
</div>
<span
class=
"issuable-header-text hide-collapsed pull-left"
>
{{
__
(
'
Notifications
'
)
}}
</span>
<loading-button
ref=
"loadingButton"
class=
"btn btn-default pull-right hide-collapsed js-issuable-subscribe-button"
:loading=
"loading"
:label=
"buttonLabel"
@
click=
"toggleSubscription"
/>
</div>
</
template
>
app/assets/javascripts/sidebar/services/sidebar_service.js
View file @
6dc9028f
...
...
@@ -7,6 +7,7 @@ export default class SidebarService {
constructor
(
endpointMap
)
{
if
(
!
SidebarService
.
singleton
)
{
this
.
endpoint
=
endpointMap
.
endpoint
;
this
.
toggleSubscriptionEndpoint
=
endpointMap
.
toggleSubscriptionEndpoint
;
this
.
moveIssueEndpoint
=
endpointMap
.
moveIssueEndpoint
;
this
.
projectsAutocompleteEndpoint
=
endpointMap
.
projectsAutocompleteEndpoint
;
...
...
@@ -36,6 +37,10 @@ export default class SidebarService {
});
}
toggleSubscription
()
{
return
Vue
.
http
.
post
(
this
.
toggleSubscriptionEndpoint
);
}
moveIssue
(
moveToProjectId
)
{
return
Vue
.
http
.
post
(
this
.
moveIssueEndpoint
,
{
move_to_project_id
:
moveToProjectId
,
...
...
app/assets/javascripts/sidebar/sidebar_bundle.js
View file @
6dc9028f
...
...
@@ -4,6 +4,8 @@ import SidebarAssignees from './components/assignees/sidebar_assignees';
import
ConfidentialIssueSidebar
from
'
./components/confidential/confidential_issue_sidebar.vue
'
;
import
SidebarMoveIssue
from
'
./lib/sidebar_move_issue
'
;
import
LockIssueSidebar
from
'
./components/lock/lock_issue_sidebar.vue
'
;
import
sidebarParticipants
from
'
./components/participants/sidebar_participants.vue
'
;
import
sidebarSubscriptions
from
'
./components/subscriptions/sidebar_subscriptions.vue
'
;
import
Translate
from
'
../vue_shared/translate
'
;
import
Mediator
from
'
./sidebar_mediator
'
;
...
...
@@ -49,6 +51,36 @@ function mountLockComponent(mediator) {
}).
$mount
(
el
);
}
function
mountParticipantsComponent
()
{
const
el
=
document
.
querySelector
(
'
.js-sidebar-participants-entry-point
'
);
if
(
!
el
)
return
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
sidebarParticipants
,
},
render
:
createElement
=>
createElement
(
'
sidebar-participants
'
,
{}),
});
}
function
mountSubscriptionsComponent
()
{
const
el
=
document
.
querySelector
(
'
.js-sidebar-subscriptions-entry-point
'
);
if
(
!
el
)
return
;
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
sidebarSubscriptions
,
},
render
:
createElement
=>
createElement
(
'
sidebar-subscriptions
'
,
{}),
});
}
function
domContentLoaded
()
{
const
sidebarOptions
=
JSON
.
parse
(
document
.
querySelector
(
'
.js-sidebar-options
'
).
innerHTML
);
const
mediator
=
new
Mediator
(
sidebarOptions
);
...
...
@@ -63,6 +95,8 @@ function domContentLoaded() {
mountConfidentialComponent
(
mediator
);
mountLockComponent
(
mediator
);
mountParticipantsComponent
();
mountSubscriptionsComponent
();
new
SidebarMoveIssue
(
mediator
,
...
...
app/assets/javascripts/sidebar/sidebar_mediator.js
View file @
6dc9028f
...
...
@@ -8,6 +8,7 @@ export default class SidebarMediator {
this
.
store
=
new
Store
(
options
);
this
.
service
=
new
Service
({
endpoint
:
options
.
endpoint
,
toggleSubscriptionEndpoint
:
options
.
toggleSubscriptionEndpoint
,
moveIssueEndpoint
:
options
.
moveIssueEndpoint
,
projectsAutocompleteEndpoint
:
options
.
projectsAutocompleteEndpoint
,
});
...
...
@@ -39,10 +40,25 @@ export default class SidebarMediator {
.
then
((
data
)
=>
{
this
.
store
.
setAssigneeData
(
data
);
this
.
store
.
setTimeTrackingData
(
data
);
this
.
store
.
setParticipantsData
(
data
);
this
.
store
.
setSubscriptionsData
(
data
);
})
.
catch
(()
=>
new
Flash
(
'
Error occurred when fetching sidebar data
'
));
}
toggleSubscription
()
{
this
.
store
.
setFetchingState
(
'
subscriptions
'
,
true
);
return
this
.
service
.
toggleSubscription
()
.
then
(()
=>
{
this
.
store
.
setSubscribedState
(
!
this
.
store
.
subscribed
);
this
.
store
.
setFetchingState
(
'
subscriptions
'
,
false
);
})
.
catch
((
err
)
=>
{
this
.
store
.
setFetchingState
(
'
subscriptions
'
,
false
);
throw
err
;
});
}
fetchAutocompleteProjects
(
searchTerm
)
{
return
this
.
service
.
getProjectsAutocomplete
(
searchTerm
)
.
then
(
response
=>
response
.
json
())
...
...
app/assets/javascripts/sidebar/stores/sidebar_store.js
View file @
6dc9028f
...
...
@@ -12,10 +12,14 @@ export default class SidebarStore {
this
.
assignees
=
[];
this
.
isFetching
=
{
assignees
:
true
,
participants
:
true
,
subscriptions
:
true
,
};
this
.
autocompleteProjects
=
[];
this
.
moveToProjectId
=
0
;
this
.
isLockDialogOpen
=
false
;
this
.
participants
=
[];
this
.
subscribed
=
null
;
SidebarStore
.
singleton
=
this
;
}
...
...
@@ -37,6 +41,20 @@ export default class SidebarStore {
this
.
humanTotalTimeSpent
=
data
.
human_total_time_spent
;
}
setParticipantsData
(
data
)
{
this
.
isFetching
.
participants
=
false
;
this
.
participants
=
data
.
participants
||
[];
}
setSubscriptionsData
(
data
)
{
this
.
isFetching
.
subscriptions
=
false
;
this
.
subscribed
=
data
.
subscribed
||
false
;
}
setFetchingState
(
key
,
value
)
{
this
.
isFetching
[
key
]
=
value
;
}
addAssignee
(
assignee
)
{
if
(
!
this
.
findAssignee
(
assignee
))
{
this
.
assignees
.
push
(
assignee
);
...
...
@@ -61,6 +79,10 @@ export default class SidebarStore {
this
.
autocompleteProjects
=
projects
;
}
setSubscribedState
(
subscribed
)
{
this
.
subscribed
=
subscribed
;
}
setMoveToProjectId
(
moveToProjectId
)
{
this
.
moveToProjectId
=
moveToProjectId
;
}
...
...
app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
View file @
6dc9028f
...
...
@@ -11,7 +11,7 @@ export default class MRWidgetService {
this
.
removeWIPResource
=
Vue
.
resource
(
endpoints
.
removeWIPPath
);
this
.
removeSourceBranchResource
=
Vue
.
resource
(
endpoints
.
sourceBranchPath
);
this
.
deploymentsResource
=
Vue
.
resource
(
endpoints
.
ciEnvironmentsStatusPath
);
this
.
pollResource
=
Vue
.
resource
(
`
${
endpoints
.
statusPath
}
?
basic=true
`
);
this
.
pollResource
=
Vue
.
resource
(
`
${
endpoints
.
statusPath
}
?
serializer=basic
`
);
this
.
mergeActionsContentResource
=
Vue
.
resource
(
endpoints
.
mergeActionsContentPath
);
}
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
6dc9028f
...
...
@@ -542,7 +542,9 @@
}
.participants-list
{
margin
:
-5px
;
display
:
flex
;
flex-wrap
:
wrap
;
margin
:
-7px
;
}
...
...
@@ -553,7 +555,7 @@
.participants-author
{
display
:
inline-block
;
padding
:
5
px
;
padding
:
7
px
;
&
:nth-of-type
(
7n
)
{
padding-right
:
0
;
...
...
app/controllers/projects/issues_controller.rb
View file @
6dc9028f
...
...
@@ -74,7 +74,7 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to
do
|
format
|
format
.
html
format
.
json
do
render
json:
serializer
.
represent
(
@issue
)
render
json:
serializer
.
represent
(
@issue
,
serializer:
params
[
:serializer
]
)
end
end
end
...
...
app/controllers/projects/merge_requests_controller.rb
View file @
6dc9028f
...
...
@@ -83,7 +83,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
format
.
json
do
Gitlab
::
PollingInterval
.
set_header
(
response
,
interval:
10_000
)
render
json:
serializer
.
represent
(
@merge_request
,
basic:
params
[
:basic
])
render
json:
serializer
.
represent
(
@merge_request
,
serializer:
params
[
:serializer
])
end
format
.
patch
do
...
...
app/helpers/issuables_helper.rb
View file @
6dc9028f
...
...
@@ -33,15 +33,17 @@ module IssuablesHelper
end
def
serialize_issuable
(
issuable
)
case
issuable
when
Issue
IssueSerializer
.
new
(
current_user:
current_user
,
project:
issuable
.
project
).
represent
(
issuable
).
to_json
when
MergeRequest
MergeRequestSerializer
.
new
(
current_user:
current_user
,
project:
issuable
.
project
)
.
represent
(
issuable
)
.
to_json
end
serializer_klass
=
case
issuable
when
Issue
IssueSerializer
when
MergeRequest
MergeRequestSerializer
end
serializer_klass
.
new
(
current_user:
current_user
,
project:
issuable
.
project
)
.
represent
(
issuable
)
.
to_json
end
def
template_dropdown_tag
(
issuable
,
&
block
)
...
...
@@ -357,7 +359,8 @@ module IssuablesHelper
def
issuable_sidebar_options
(
issuable
,
can_edit_issuable
)
{
endpoint:
"
#{
issuable_json_path
(
issuable
)
}
?basic=true"
,
endpoint:
"
#{
issuable_json_path
(
issuable
)
}
?serializer=sidebar"
,
toggleSubscriptionEndpoint:
toggle_subscription_path
(
issuable
),
moveIssueEndpoint:
move_namespace_project_issue_path
(
namespace_id:
issuable
.
project
.
namespace
.
to_param
,
project_id:
issuable
.
project
,
id:
issuable
),
projectsAutocompleteEndpoint:
autocomplete_projects_path
(
project_id:
@project
.
id
),
editable:
can_edit_issuable
,
...
...
app/models/concerns/subscribable.rb
View file @
6dc9028f
...
...
@@ -13,6 +13,8 @@ module Subscribable
end
def
subscribed?
(
user
,
project
=
nil
)
return
false
unless
user
if
subscription
=
subscriptions
.
find_by
(
user:
user
,
project:
project
)
subscription
.
subscribed
else
...
...
app/serializers/issuable_sidebar_entity.rb
0 → 100644
View file @
6dc9028f
class
IssuableSidebarEntity
<
Grape
::
Entity
include
RequestAwareEntity
expose
:participants
,
using:
::
API
::
Entities
::
UserBasic
do
|
issuable
|
issuable
.
participants
(
request
.
current_user
)
end
expose
:subscribed
do
|
issuable
|
issuable
.
subscribed?
(
request
.
current_user
,
issuable
.
project
)
end
expose
:time_estimate
expose
:total_time_spent
expose
:human_time_estimate
expose
:human_total_time_spent
end
app/serializers/issue_serializer.rb
View file @
6dc9028f
class
IssueSerializer
<
BaseSerializer
entity
IssueEntity
# This overrided method takes care of which entity should be used
# to serialize the `issue` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def
represent
(
merge_request
,
opts
=
{})
entity
=
case
opts
[
:serializer
]
when
'sidebar'
IssueSidebarEntity
else
IssueEntity
end
super
(
merge_request
,
opts
,
entity
)
end
end
app/serializers/issue_sidebar_entity.rb
0 → 100644
View file @
6dc9028f
class
IssueSidebarEntity
<
IssuableSidebarEntity
expose
:assignees
,
using:
API
::
Entities
::
UserBasic
end
app/serializers/merge_request_basic_entity.rb
View file @
6dc9028f
class
MergeRequestBasicEntity
<
Grape
::
Entity
class
MergeRequestBasicEntity
<
IssuableSidebar
Entity
expose
:assignee_id
expose
:merge_status
expose
:merge_error
expose
:state
expose
:source_branch_exists?
,
as: :source_branch_exists
expose
:time_estimate
expose
:total_time_spent
expose
:human_time_estimate
expose
:human_total_time_spent
end
app/serializers/merge_request_serializer.rb
View file @
6dc9028f
...
...
@@ -3,7 +3,14 @@ class MergeRequestSerializer < BaseSerializer
# to serialize the `merge_request` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def
represent
(
merge_request
,
opts
=
{})
entity
=
opts
[
:basic
]
?
MergeRequestBasicEntity
:
MergeRequestEntity
entity
=
case
opts
[
:serializer
]
when
'basic'
,
'sidebar'
MergeRequestBasicEntity
else
MergeRequestEntity
end
super
(
merge_request
,
opts
,
entity
)
end
end
app/views/shared/issuable/_participants.html.haml
deleted
100644 → 0
View file @
74a0e855
-
participants_row
=
7
-
participants_size
=
participants
.
size
-
participants_extra
=
participants_size
-
participants_row
.block.participants
.sidebar-collapsed-icon
=
icon
(
'users'
)
%span
=
participants
.
count
.title.hide-collapsed
=
pluralize
participants
.
count
,
"participant"
.hide-collapsed.participants-list
-
participants
.
each
do
|
participant
|
.participants-author.js-participants-author
=
link_to_member
(
@project
,
participant
,
name:
false
,
size:
24
,
lazy_load:
true
)
-
if
participants_extra
>
0
.hide-collapsed.participants-more
%button
.btn-transparent.btn-blank.js-participants-more
{
type:
'button'
,
data:
{
original_text:
"+ #{participants_size - 7} more"
,
less_text:
"- show less"
}
}
+
#{
participants_extra
}
more
app/views/shared/issuable/_sidebar.html.haml
View file @
6dc9028f
...
...
@@ -123,17 +123,10 @@
%script
#js-lock-issue-data
{
type:
"application/json"
}=
{
is_locked:
issuable
.
discussion_locked?
,
is_editable:
can_edit_issuable
}.
to_json
.
html_safe
#js-lock-entry-point
=
render
"shared/issuable/participants"
,
participants:
issuable
.
participants
(
current_user
)
.js-sidebar-participants-entry-point
-
if
current_user
-
subscribed
=
issuable
.
subscribed?
(
current_user
,
@project
)
.block.light.subscription
{
data:
{
url:
toggle_subscription_path
(
issuable
)
}
}
.sidebar-collapsed-icon
=
icon
(
'rss'
,
'aria-hidden'
:
'true'
)
%span
.issuable-header-text.hide-collapsed.pull-left
Notifications
-
subscribtion_status
=
subscribed
?
'subscribed'
:
'unsubscribed'
%button
.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed
{
type:
"button"
}
%span
=
subscribed
?
'Unsubscribe'
:
'Subscribe'
.js-sidebar-subscriptions-entry-point
-
project_ref
=
cross_project_reference
(
@project
,
issuable
)
.block.project-reference
...
...
changelogs/unreleased/23206-load-participants-async.yml
0 → 100644
View file @
6dc9028f
---
title
:
Update participants and subscriptions button in issuable sidebar to be async
merge_request
:
14836
author
:
type
:
changed
features/steps/project/issues/issues.rb
View file @
6dc9028f
...
...
@@ -20,11 +20,13 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step
'I should see that I am subscribed'
do
expect
(
find
(
'.issuable-subscribe-button span'
)).
to
have_content
'Unsubscribe'
wait_for_requests
expect
(
find
(
'.js-issuable-subscribe-button span'
)).
to
have_content
'Unsubscribe'
end
step
'I should see that I am unsubscribed'
do
expect
(
find
(
'.issuable-subscribe-button span'
)).
to
have_content
'Subscribe'
wait_for_requests
expect
(
find
(
'.js-issuable-subscribe-button span'
)).
to
have_content
'Subscribe'
end
step
'I click link "Closed"'
do
...
...
spec/controllers/projects/merge_requests_controller_spec.rb
View file @
6dc9028f
...
...
@@ -83,15 +83,15 @@ describe Projects::MergeRequestsController do
end
describe
'as json'
do
context
'with basic param'
do
context
'with basic
serializer
param'
do
it
'renders basic MR entity as json'
do
go
(
basic:
true
,
format: :json
)
go
(
serializer:
'basic'
,
format: :json
)
expect
(
response
).
to
match_response_schema
(
'entities/merge_request_basic'
)
end
end
context
'without basic param'
do
context
'without basic
serializer
param'
do
it
'renders the merge request in the json format'
do
go
(
format: :json
)
...
...
spec/features/boards/boards_spec.rb
View file @
6dc9028f
...
...
@@ -538,7 +538,7 @@ describe 'Issue Boards', :js do
end
it
'does not show create new list'
do
expect
(
page
).
not_to
have_
selector
(
'.js-new-board-list'
)
expect
(
page
).
not_to
have_
button
(
'.js-new-board-list'
)
end
it
'does not allow dragging'
do
...
...
spec/features/projects/merge_requests/user_manages_subscription_spec.rb
View file @
6dc9028f
...
...
@@ -13,7 +13,7 @@ describe 'User manages subscription', :js do
end
it
'toggles subscription'
do
subscribe_button
=
find
(
'.
issuable-subscribe-button spa
n'
)
subscribe_button
=
find
(
'.
js-issuable-subscribe-butto
n'
)
expect
(
subscribe_button
).
to
have_content
(
'Subscribe'
)
...
...
spec/fixtures/api/schemas/entities/issue.json
0 → 100644
View file @
6dc9028f
{
"type"
:
"object"
,
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"iid"
:
{
"type"
:
"integer"
},
"author_id"
:
{
"type"
:
"integer"
},
"description"
:
{
"type"
:
[
"string"
,
"null"
]
},
"lock_version"
:
{
"type"
:
[
"string"
,
"null"
]
},
"milestone_id"
:
{
"type"
:
[
"string"
,
"null"
]
},
"title"
:
{
"type"
:
"string"
},
"moved_to_id"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"project_id"
:
{
"type"
:
"integer"
},
"web_url"
:
{
"type"
:
"string"
},
"state"
:
{
"type"
:
"string"
},
"create_note_path"
:
{
"type"
:
"string"
},
"preview_note_path"
:
{
"type"
:
"string"
},
"current_user"
:
{
"type"
:
"object"
,
"properties"
:
{
"can_create_note"
:
{
"type"
:
"boolean"
},
"can_update"
:
{
"type"
:
"boolean"
}
}
},
"created_at"
:
{
"type"
:
"date-time"
},
"updated_at"
:
{
"type"
:
"date-time"
},
"branch_name"
:
{
"type"
:
[
"string"
,
"null"
]
},
"due_date"
:
{
"type"
:
"date"
},
"confidential"
:
{
"type"
:
"boolean"
},
"discussion_locked"
:
{
"type"
:
[
"boolean"
,
"null"
]
},
"updated_by_id"
:
{
"type"
:
[
"string"
,
"null"
]
},
"deleted_at"
:
{
"type"
:
[
"string"
,
"null"
]
},
"time_estimate"
:
{
"type"
:
"integer"
},
"total_time_spent"
:
{
"type"
:
"integer"
},
"human_time_estimate"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"human_total_time_spent"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"milestone"
:
{
"type"
:
[
"object"
,
"null"
]
},
"labels"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"label.json"
}
},
"assignees"
:
{
"type"
:
[
"array"
,
"null"
]
}
},
"additionalProperties"
:
false
}
spec/fixtures/api/schemas/entities/issue_sidebar.json
0 → 100644
View file @
6dc9028f
{
"type"
:
"object"
,
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"iid"
:
{
"type"
:
"integer"
},
"subscribed"
:
{
"type"
:
"boolean"
},
"time_estimate"
:
{
"type"
:
"integer"
},
"total_time_spent"
:
{
"type"
:
"integer"
},
"human_time_estimate"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"human_total_time_spent"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"participants"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"../public_api/v4/user/basic.json"
}
},
"assignees"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"../public_api/v4/user/basic.json"
}
}
},
"additionalProperties"
:
false
}
spec/fixtures/api/schemas/entities/label.json
0 → 100644
View file @
6dc9028f
{
"type"
:
"object"
,
"required"
:
[
"id"
,
"color"
,
"description"
,
"title"
,
"priority"
],
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"color"
:
{
"type"
:
"string"
,
"pattern"
:
"^#[0-9A-Fa-f]{3}{1,2}$"
},
"description"
:
{
"type"
:
[
"string"
,
"null"
]
},
"text_color"
:
{
"type"
:
"string"
,
"pattern"
:
"^#[0-9A-Fa-f]{3}{1,2}$"
},
"type"
:
{
"type"
:
"string"
},
"title"
:
{
"type"
:
"string"
},
"priority"
:
{
"type"
:
[
"integer"
,
"null"
]
}
},
"additionalProperties"
:
false
}
\ No newline at end of file
spec/fixtures/api/schemas/entities/merge_request_basic.json
View file @
6dc9028f
...
...
@@ -9,7 +9,9 @@
"human_time_estimate"
:
{
"type"
:
[
"string"
,
"null"
]
},
"human_total_time_spent"
:
{
"type"
:
[
"string"
,
"null"
]
},
"merge_error"
:
{
"type"
:
[
"string"
,
"null"
]
},
"assignee_id"
:
{
"type"
:
[
"integer"
,
"null"
]
}
"assignee_id"
:
{
"type"
:
[
"integer"
,
"null"
]
},
"subscribed"
:
{
"type"
:
[
"boolean"
,
"null"
]
},
"participants"
:
{
"type"
:
"array"
}
},
"additionalProperties"
:
false
}
spec/fixtures/api/schemas/issue.json
View file @
6dc9028f
...
...
@@ -19,32 +19,7 @@
},
"labels"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"object"
,
"required"
:
[
"id"
,
"color"
,
"description"
,
"title"
,
"priority"
],
"properties"
:
{
"id"
:
{
"type"
:
"integer"
},
"color"
:
{
"type"
:
"string"
,
"pattern"
:
"^#[0-9A-Fa-f]{3}{1,2}+$"
},
"description"
:
{
"type"
:
[
"string"
,
"null"
]
},
"text_color"
:
{
"type"
:
"string"
,
"pattern"
:
"^#[0-9A-Fa-f]{3}{1,2}+$"
},
"type"
:
{
"type"
:
"string"
},
"title"
:
{
"type"
:
"string"
},
"priority"
:
{
"type"
:
[
"integer"
,
"null"
]
}
},
"additionalProperties"
:
false
}
"items"
:
{
"$ref"
:
"entities/label.json"
}
},
"assignee"
:
{
"id"
:
{
"type"
:
"integet"
},
...
...
spec/javascripts/issuable_context_spec.js
deleted
100644 → 0
View file @
74a0e855
import
$
from
'
jquery
'
;
import
IssuableContext
from
'
~/issuable_context
'
;
describe
(
'
IssuableContext
'
,
()
=>
{
describe
(
'
toggleHiddenParticipants
'
,
()
=>
{
const
event
=
jasmine
.
createSpyObj
(
'
event
'
,
[
'
preventDefault
'
]);
beforeEach
(()
=>
{
spyOn
(
$
.
fn
,
'
data
'
).
and
.
returnValue
(
'
data
'
);
spyOn
(
$
.
fn
,
'
text
'
).
and
.
returnValue
(
'
data
'
);
});
afterEach
(()
=>
{
gl
.
lazyLoader
=
undefined
;
});
it
(
'
calls loadCheck if lazyLoader is set
'
,
()
=>
{
gl
.
lazyLoader
=
jasmine
.
createSpyObj
(
'
lazyLoader
'
,
[
'
loadCheck
'
]);
IssuableContext
.
prototype
.
toggleHiddenParticipants
(
event
);
expect
(
gl
.
lazyLoader
.
loadCheck
).
toHaveBeenCalled
();
});
it
(
'
does not throw if lazyLoader is not defined
'
,
()
=>
{
gl
.
lazyLoader
=
undefined
;
const
toggle
=
IssuableContext
.
prototype
.
toggleHiddenParticipants
.
bind
(
null
,
event
);
expect
(
toggle
).
not
.
toThrow
();
});
});
});
spec/javascripts/sidebar/mock_data.js
View file @
6dc9028f
...
...
@@ -109,12 +109,14 @@ const sidebarMockData = {
labels
:
[],
web_url
:
'
/root/some-project/issues/5
'
,
},
'
/gitlab-org/gitlab-shell/issues/5/toggle_subscription
'
:
{},
},
};
export
default
{
mediator
:
{
endpoint
:
'
/gitlab-org/gitlab-shell/issues/5.json
'
,
toggleSubscriptionEndpoint
:
'
/gitlab-org/gitlab-shell/issues/5/toggle_subscription
'
,
moveIssueEndpoint
:
'
/gitlab-org/gitlab-shell/issues/5/move
'
,
projectsAutocompleteEndpoint
:
'
/autocomplete/projects?project_id=15
'
,
editable
:
true
,
...
...
spec/javascripts/sidebar/participants_spec.js
0 → 100644
View file @
6dc9028f
import
Vue
from
'
vue
'
;
import
participants
from
'
~/sidebar/components/participants/participants.vue
'
;
import
mountComponent
from
'
../helpers/vue_mount_component_helper
'
;
const
PARTICIPANT
=
{
id
:
1
,
state
:
'
active
'
,
username
:
'
marcene
'
,
name
:
'
Allie Will
'
,
web_url
:
'
foo.com
'
,
avatar_url
:
'
gravatar.com/avatar/xxx
'
,
};
const
PARTICIPANT_LIST
=
[
PARTICIPANT
,
{
...
PARTICIPANT
,
id
:
2
},
{
...
PARTICIPANT
,
id
:
3
},
];
describe
(
'
Participants
'
,
function
()
{
let
vm
;
let
Participants
;
beforeEach
(()
=>
{
Participants
=
Vue
.
extend
(
participants
);
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
collapsed sidebar state
'
,
()
=>
{
it
(
'
shows loading spinner when loading
'
,
()
=>
{
vm
=
mountComponent
(
Participants
,
{
loading
:
true
,
});
expect
(
vm
.
$el
.
querySelector
(
'
.js-participants-collapsed-loading-icon
'
)).
toBeDefined
();
});
it
(
'
shows participant count when given
'
,
()
=>
{
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
});
const
countEl
=
vm
.
$el
.
querySelector
(
'
.js-participants-collapsed-count
'
);
expect
(
countEl
.
textContent
.
trim
()).
toBe
(
`
${
PARTICIPANT_LIST
.
length
}
`
);
});
it
(
'
shows full participant count when there are hidden participants
'
,
()
=>
{
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
numberOfLessParticipants
:
1
,
});
const
countEl
=
vm
.
$el
.
querySelector
(
'
.js-participants-collapsed-count
'
);
expect
(
countEl
.
textContent
.
trim
()).
toBe
(
`
${
PARTICIPANT_LIST
.
length
}
`
);
});
});
describe
(
'
expanded sidebar state
'
,
()
=>
{
it
(
'
shows loading spinner when loading
'
,
()
=>
{
vm
=
mountComponent
(
Participants
,
{
loading
:
true
,
});
expect
(
vm
.
$el
.
querySelector
(
'
.js-participants-expanded-loading-icon
'
)).
toBeDefined
();
});
it
(
'
when only showing visible participants, shows an avatar only for each participant under the limit
'
,
(
done
)
=>
{
const
numberOfLessParticipants
=
2
;
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
numberOfLessParticipants
,
});
vm
.
isShowingMoreParticipants
=
false
;
Vue
.
nextTick
()
.
then
(()
=>
{
const
participantEls
=
vm
.
$el
.
querySelectorAll
(
'
.js-participants-author
'
);
expect
(
participantEls
.
length
).
toBe
(
numberOfLessParticipants
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
when only showing all participants, each has an avatar
'
,
(
done
)
=>
{
const
numberOfLessParticipants
=
2
;
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
numberOfLessParticipants
,
});
vm
.
isShowingMoreParticipants
=
true
;
Vue
.
nextTick
()
.
then
(()
=>
{
const
participantEls
=
vm
.
$el
.
querySelectorAll
(
'
.js-participants-author
'
);
expect
(
participantEls
.
length
).
toBe
(
PARTICIPANT_LIST
.
length
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
does not have more participants link when they can all be shown
'
,
()
=>
{
const
numberOfLessParticipants
=
100
;
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
numberOfLessParticipants
,
});
const
moreParticipantLink
=
vm
.
$el
.
querySelector
(
'
.js-toggle-participants-button
'
);
expect
(
PARTICIPANT_LIST
.
length
).
toBeLessThan
(
numberOfLessParticipants
);
expect
(
moreParticipantLink
).
toBeNull
();
});
it
(
'
when too many participants, has more participants link to show more
'
,
(
done
)
=>
{
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
numberOfLessParticipants
:
2
,
});
vm
.
isShowingMoreParticipants
=
false
;
Vue
.
nextTick
()
.
then
(()
=>
{
const
moreParticipantLink
=
vm
.
$el
.
querySelector
(
'
.js-toggle-participants-button
'
);
expect
(
moreParticipantLink
.
textContent
.
trim
()).
toBe
(
'
+ 1 more
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
when too many participants and already showing them, has more participants link to show less
'
,
(
done
)
=>
{
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
numberOfLessParticipants
:
2
,
});
vm
.
isShowingMoreParticipants
=
true
;
Vue
.
nextTick
()
.
then
(()
=>
{
const
moreParticipantLink
=
vm
.
$el
.
querySelector
(
'
.js-toggle-participants-button
'
);
expect
(
moreParticipantLink
.
textContent
.
trim
()).
toBe
(
'
- show less
'
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
clicking more participants link emits event
'
,
()
=>
{
vm
=
mountComponent
(
Participants
,
{
loading
:
false
,
participants
:
PARTICIPANT_LIST
,
numberOfLessParticipants
:
2
,
});
const
moreParticipantLink
=
vm
.
$el
.
querySelector
(
'
.js-toggle-participants-button
'
);
expect
(
vm
.
isShowingMoreParticipants
).
toBe
(
false
);
moreParticipantLink
.
click
();
expect
(
vm
.
isShowingMoreParticipants
).
toBe
(
true
);
});
});
});
spec/javascripts/sidebar/sidebar_mediator_spec.js
View file @
6dc9028f
...
...
@@ -57,8 +57,8 @@ describe('Sidebar mediator', () => {
.
then
(()
=>
{
expect
(
this
.
mediator
.
service
.
getProjectsAutocomplete
).
toHaveBeenCalledWith
(
searchTerm
);
expect
(
this
.
mediator
.
store
.
setAutocompleteProjects
).
toHaveBeenCalled
();
done
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
...
...
@@ -72,8 +72,21 @@ describe('Sidebar mediator', () => {
.
then
(()
=>
{
expect
(
this
.
mediator
.
service
.
moveIssue
).
toHaveBeenCalledWith
(
moveToProjectId
);
expect
(
gl
.
utils
.
visitUrl
).
toHaveBeenCalledWith
(
'
/root/some-project/issues/5
'
);
done
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
toggle subscription
'
,
(
done
)
=>
{
this
.
mediator
.
store
.
setSubscribedState
(
false
);
spyOn
(
this
.
mediator
.
service
,
'
toggleSubscription
'
).
and
.
callThrough
();
this
.
mediator
.
toggleSubscription
()
.
then
(()
=>
{
expect
(
this
.
mediator
.
service
.
toggleSubscription
).
toHaveBeenCalled
();
expect
(
this
.
mediator
.
store
.
subscribed
).
toEqual
(
true
);
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
spec/javascripts/sidebar/sidebar_service_spec.js
View file @
6dc9028f
...
...
@@ -7,6 +7,7 @@ describe('Sidebar service', () => {
Vue
.
http
.
interceptors
.
push
(
Mock
.
sidebarMockInterceptor
);
this
.
service
=
new
SidebarService
({
endpoint
:
'
/gitlab-org/gitlab-shell/issues/5.json
'
,
toggleSubscriptionEndpoint
:
'
/gitlab-org/gitlab-shell/issues/5/toggle_subscription
'
,
moveIssueEndpoint
:
'
/gitlab-org/gitlab-shell/issues/5/move
'
,
projectsAutocompleteEndpoint
:
'
/autocomplete/projects?project_id=15
'
,
});
...
...
@@ -23,6 +24,7 @@ describe('Sidebar service', () => {
expect
(
resp
).
toBeDefined
();
done
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
...
...
@@ -30,8 +32,8 @@ describe('Sidebar service', () => {
this
.
service
.
update
(
'
issue[assignee_ids]
'
,
[
1
])
.
then
((
resp
)
=>
{
expect
(
resp
).
toBeDefined
();
done
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
...
...
@@ -39,8 +41,8 @@ describe('Sidebar service', () => {
this
.
service
.
getProjectsAutocomplete
()
.
then
((
resp
)
=>
{
expect
(
resp
).
toBeDefined
();
done
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
...
...
@@ -48,8 +50,17 @@ describe('Sidebar service', () => {
this
.
service
.
moveIssue
(
123
)
.
then
((
resp
)
=>
{
expect
(
resp
).
toBeDefined
();
done
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
it
(
'
toggles the subscription
'
,
(
done
)
=>
{
this
.
service
.
toggleSubscription
()
.
then
((
resp
)
=>
{
expect
(
resp
).
toBeDefined
();
})
.
then
(
done
)
.
catch
(
done
.
fail
);
});
});
spec/javascripts/sidebar/sidebar_store_spec.js
View file @
6dc9028f
...
...
@@ -2,21 +2,36 @@ import SidebarStore from '~/sidebar/stores/sidebar_store';
import
Mock
from
'
./mock_data
'
;
import
UsersMockHelper
from
'
../helpers/user_mock_data_helper
'
;
describe
(
'
Sidebar store
'
,
()
=>
{
const
assignee
=
{
id
:
2
,
name
:
'
gitlab user 2
'
,
username
:
'
gitlab2
'
,
avatar_url
:
'
http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon
'
,
};
const
anotherAssignee
=
{
id
:
3
,
name
:
'
gitlab user 3
'
,
username
:
'
gitlab3
'
,
avatar_url
:
'
http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon
'
,
};
const
ASSIGNEE
=
{
id
:
2
,
name
:
'
gitlab user 2
'
,
username
:
'
gitlab2
'
,
avatar_url
:
'
http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon
'
,
};
const
ANOTHER_ASSINEE
=
{
id
:
3
,
name
:
'
gitlab user 3
'
,
username
:
'
gitlab3
'
,
avatar_url
:
'
http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon
'
,
};
const
PARTICIPANT
=
{
id
:
1
,
state
:
'
active
'
,
username
:
'
marcene
'
,
name
:
'
Allie Will
'
,
web_url
:
'
foo.com
'
,
avatar_url
:
'
gravatar.com/avatar/xxx
'
,
};
const
PARTICIPANT_LIST
=
[
PARTICIPANT
,
{
...
PARTICIPANT
,
id
:
2
},
{
...
PARTICIPANT
,
id
:
3
},
];
describe
(
'
Sidebar store
'
,
()
=>
{
beforeEach
(()
=>
{
this
.
store
=
new
SidebarStore
({
currentUser
:
{
...
...
@@ -40,23 +55,23 @@ describe('Sidebar store', () => {
});
it
(
'
adds a new assignee
'
,
()
=>
{
this
.
store
.
addAssignee
(
assignee
);
this
.
store
.
addAssignee
(
ASSIGNEE
);
expect
(
this
.
store
.
assignees
.
length
).
toEqual
(
1
);
});
it
(
'
removes an assignee
'
,
()
=>
{
this
.
store
.
removeAssignee
(
assignee
);
this
.
store
.
removeAssignee
(
ASSIGNEE
);
expect
(
this
.
store
.
assignees
.
length
).
toEqual
(
0
);
});
it
(
'
finds an existent assignee
'
,
()
=>
{
let
foundAssignee
;
this
.
store
.
addAssignee
(
assignee
);
foundAssignee
=
this
.
store
.
findAssignee
(
assignee
);
this
.
store
.
addAssignee
(
ASSIGNEE
);
foundAssignee
=
this
.
store
.
findAssignee
(
ASSIGNEE
);
expect
(
foundAssignee
).
toBeDefined
();
expect
(
foundAssignee
).
toEqual
(
assignee
);
foundAssignee
=
this
.
store
.
findAssignee
(
anotherAssignee
);
expect
(
foundAssignee
).
toEqual
(
ASSIGNEE
);
foundAssignee
=
this
.
store
.
findAssignee
(
ANOTHER_ASSINEE
);
expect
(
foundAssignee
).
toBeUndefined
();
});
...
...
@@ -65,6 +80,28 @@ describe('Sidebar store', () => {
expect
(
this
.
store
.
assignees
.
length
).
toEqual
(
0
);
});
it
(
'
sets participants data
'
,
()
=>
{
expect
(
this
.
store
.
participants
.
length
).
toEqual
(
0
);
this
.
store
.
setParticipantsData
({
participants
:
PARTICIPANT_LIST
,
});
expect
(
this
.
store
.
isFetching
.
participants
).
toEqual
(
false
);
expect
(
this
.
store
.
participants
.
length
).
toEqual
(
PARTICIPANT_LIST
.
length
);
});
it
(
'
sets subcriptions data
'
,
()
=>
{
expect
(
this
.
store
.
subscribed
).
toEqual
(
null
);
this
.
store
.
setSubscriptionsData
({
subscribed
:
true
,
});
expect
(
this
.
store
.
isFetching
.
subscriptions
).
toEqual
(
false
);
expect
(
this
.
store
.
subscribed
).
toEqual
(
true
);
});
it
(
'
set assigned data
'
,
()
=>
{
const
users
=
{
assignees
:
UsersMockHelper
.
createNumberRandomUsers
(
3
),
...
...
@@ -75,6 +112,14 @@ describe('Sidebar store', () => {
expect
(
this
.
store
.
assignees
.
length
).
toEqual
(
3
);
});
it
(
'
sets fetching state
'
,
()
=>
{
expect
(
this
.
store
.
isFetching
.
participants
).
toEqual
(
true
);
this
.
store
.
setFetchingState
(
'
participants
'
,
false
);
expect
(
this
.
store
.
isFetching
.
participants
).
toEqual
(
false
);
});
it
(
'
set time tracking data
'
,
()
=>
{
this
.
store
.
setTimeTrackingData
(
Mock
.
time
);
expect
(
this
.
store
.
timeEstimate
).
toEqual
(
Mock
.
time
.
time_estimate
);
...
...
@@ -90,6 +135,14 @@ describe('Sidebar store', () => {
expect
(
this
.
store
.
autocompleteProjects
).
toEqual
(
projects
);
});
it
(
'
sets subscribed state
'
,
()
=>
{
expect
(
this
.
store
.
subscribed
).
toEqual
(
null
);
this
.
store
.
setSubscribedState
(
true
);
expect
(
this
.
store
.
subscribed
).
toEqual
(
true
);
});
it
(
'
set move to project ID
'
,
()
=>
{
const
projectId
=
7
;
this
.
store
.
setMoveToProjectId
(
projectId
);
...
...
spec/javascripts/sidebar/sidebar_subscriptions_spec.js
0 → 100644
View file @
6dc9028f
import
Vue
from
'
vue
'
;
import
sidebarSubscriptions
from
'
~/sidebar/components/subscriptions/sidebar_subscriptions.vue
'
;
import
SidebarMediator
from
'
~/sidebar/sidebar_mediator
'
;
import
SidebarService
from
'
~/sidebar/services/sidebar_service
'
;
import
SidebarStore
from
'
~/sidebar/stores/sidebar_store
'
;
import
eventHub
from
'
~/sidebar/event_hub
'
;
import
mountComponent
from
'
../helpers/vue_mount_component_helper
'
;
import
Mock
from
'
./mock_data
'
;
describe
(
'
Sidebar Subscriptions
'
,
function
()
{
let
vm
;
let
SidebarSubscriptions
;
beforeEach
(()
=>
{
SidebarSubscriptions
=
Vue
.
extend
(
sidebarSubscriptions
);
// Setup the stores, services, etc
// eslint-disable-next-line no-new
new
SidebarMediator
(
Mock
.
mediator
);
});
afterEach
(()
=>
{
vm
.
$destroy
();
SidebarService
.
singleton
=
null
;
SidebarStore
.
singleton
=
null
;
SidebarMediator
.
singleton
=
null
;
});
it
(
'
calls the mediator toggleSubscription on event
'
,
()
=>
{
spyOn
(
SidebarMediator
.
prototype
,
'
toggleSubscription
'
).
and
.
returnValue
(
Promise
.
resolve
());
vm
=
mountComponent
(
SidebarSubscriptions
,
{});
eventHub
.
$emit
(
'
toggleSubscription
'
);
expect
(
SidebarMediator
.
prototype
.
toggleSubscription
).
toHaveBeenCalled
();
});
});
spec/javascripts/sidebar/subscriptions_spec.js
0 → 100644
View file @
6dc9028f
import
Vue
from
'
vue
'
;
import
subscriptions
from
'
~/sidebar/components/subscriptions/subscriptions.vue
'
;
import
mountComponent
from
'
../helpers/vue_mount_component_helper
'
;
describe
(
'
Subscriptions
'
,
function
()
{
let
vm
;
let
Subscriptions
;
beforeEach
(()
=>
{
Subscriptions
=
Vue
.
extend
(
subscriptions
);
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
shows loading spinner when loading
'
,
()
=>
{
vm
=
mountComponent
(
Subscriptions
,
{
loading
:
true
,
subscribed
:
undefined
,
});
expect
(
vm
.
$refs
.
loadingButton
.
loading
).
toBe
(
true
);
expect
(
vm
.
$refs
.
loadingButton
.
label
).
toBeUndefined
();
});
it
(
'
has "Subscribe" text when currently not subscribed
'
,
()
=>
{
vm
=
mountComponent
(
Subscriptions
,
{
subscribed
:
false
,
});
expect
(
vm
.
$refs
.
loadingButton
.
label
).
toBe
(
'
Subscribe
'
);
});
it
(
'
has "Unsubscribe" text when currently not subscribed
'
,
()
=>
{
vm
=
mountComponent
(
Subscriptions
,
{
subscribed
:
true
,
});
expect
(
vm
.
$refs
.
loadingButton
.
label
).
toBe
(
'
Unsubscribe
'
);
});
});
spec/models/concerns/subscribable_spec.rb
View file @
6dc9028f
...
...
@@ -6,6 +6,12 @@ describe Subscribable, 'Subscribable' do
let
(
:user_1
)
{
create
(
:user
)
}
describe
'#subscribed?'
do
context
'without user'
do
it
'returns false'
do
expect
(
resource
.
subscribed?
(
nil
,
project
)).
to
be_falsey
end
end
context
'without project'
do
it
'returns false when no subscription exists'
do
expect
(
resource
.
subscribed?
(
user_1
)).
to
be_falsey
...
...
spec/serializers/issue_serializer_spec.rb
0 → 100644
View file @
6dc9028f
require
'spec_helper'
describe
IssueSerializer
do
let
(
:resource
)
{
create
(
:issue
)
}
let
(
:user
)
{
create
(
:user
)
}
let
(
:json_entity
)
do
described_class
.
new
(
current_user:
user
)
.
represent
(
resource
,
serializer:
serializer
)
.
with_indifferent_access
end
context
'non-sidebar issue serialization'
do
let
(
:serializer
)
{
nil
}
it
'matches issue json schema'
do
expect
(
json_entity
).
to
match_schema
(
'entities/issue'
)
end
end
context
'sidebar issue serialization'
do
let
(
:serializer
)
{
'sidebar'
}
it
'matches sidebar issue json schema'
do
expect
(
json_entity
).
to
match_schema
(
'entities/issue_sidebar'
)
end
end
end
spec/serializers/merge_request_basic_serializer_spec.rb
View file @
6dc9028f
...
...
@@ -4,9 +4,13 @@ describe MergeRequestBasicSerializer do
let
(
:resource
)
{
create
(
:merge_request
)
}
let
(
:user
)
{
create
(
:user
)
}
subject
{
described_class
.
new
.
represent
(
resource
)
}
let
(
:json_entity
)
do
described_class
.
new
(
current_user:
user
)
.
represent
(
resource
,
serializer:
'basic'
)
.
with_indifferent_access
end
it
'
has important MergeRequest attributes
'
do
expect
(
subject
).
to
include
(
:merge_status
)
it
'
matches basic merge request json
'
do
expect
(
json_entity
).
to
match_schema
(
'entities/merge_request_basic'
)
end
end
spec/serializers/merge_request_serializer_spec.rb
View file @
6dc9028f
...
...
@@ -9,11 +9,11 @@ describe MergeRequestSerializer do
end
describe
'#represent'
do
let
(
:opts
)
{
{
basic:
basic
}
}
subject
{
serializer
.
represent
(
merge_request
,
basic:
basic
)
}
let
(
:opts
)
{
{
serializer:
serializer_entity
}
}
subject
{
serializer
.
represent
(
merge_request
,
serializer:
serializer_entity
)
}
context
'when
basic param is truthy
'
do
let
(
:
basic
)
{
true
}
context
'when
passing basic serializer param
'
do
let
(
:
serializer_entity
)
{
'basic'
}
it
'calls super class #represent with correct params'
do
expect_any_instance_of
(
BaseSerializer
).
to
receive
(
:represent
)
...
...
@@ -23,8 +23,8 @@ describe MergeRequestSerializer do
end
end
context
'when
basic
param is falsy'
do
let
(
:
basic
)
{
false
}
context
'when
serializer
param is falsy'
do
let
(
:
serializer_entity
)
{
nil
}
it
'calls super class #represent with correct params'
do
expect_any_instance_of
(
BaseSerializer
).
to
receive
(
:represent
)
...
...
spec/views/shared/issuable/_participants.html.haml.rb
deleted
100644 → 0
View file @
74a0e855
require
'spec_helper'
require
'nokogiri'
describe
'shared/issuable/_participants.html.haml'
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:participants
)
{
create_list
(
:user
,
100
)
}
before
do
allow
(
view
).
to
receive_messages
(
project:
project
,
participants:
participants
)
end
it
'renders lazy loaded avatars'
do
render
'shared/issuable/participants'
html
=
Nokogiri
::
HTML
(
rendered
)
avatars
=
html
.
css
(
'.participants-author img'
)
avatars
.
each
do
|
avatar
|
expect
(
avatar
[
:class
]).
to
include
(
'lazy'
)
expect
(
avatar
[
:src
]).
to
eql
(
LazyImageTagHelper
.
placeholder_image
)
expect
(
avatar
[
:"data-src"
]).
to
match
(
'http://www.gravatar.com/avatar/'
)
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