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
0
Merge Requests
0
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
Léo-Paul Géneau
gitlab-ce
Commits
de553961
Commit
de553961
authored
Oct 04, 2017
by
Kushal Pandya
Committed by
Bob Van Landuyt
Oct 04, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Groups tree enhancements for Groups Dashboard and Group Homepage
parent
67815272
Changes
48
Hide whitespace changes
Inline
Side-by-side
Showing
48 changed files
with
2753 additions
and
891 deletions
+2753
-891
app/assets/javascripts/filterable_list.js
app/assets/javascripts/filterable_list.js
+4
-3
app/assets/javascripts/groups/components/app.vue
app/assets/javascripts/groups/components/app.vue
+191
-0
app/assets/javascripts/groups/components/group_folder.vue
app/assets/javascripts/groups/components/group_folder.vue
+31
-7
app/assets/javascripts/groups/components/group_item.vue
app/assets/javascripts/groups/components/group_item.vue
+65
-163
app/assets/javascripts/groups/components/groups.vue
app/assets/javascripts/groups/components/groups.vue
+19
-4
app/assets/javascripts/groups/components/item_actions.vue
app/assets/javascripts/groups/components/item_actions.vue
+92
-0
app/assets/javascripts/groups/components/item_caret.vue
app/assets/javascripts/groups/components/item_caret.vue
+25
-0
app/assets/javascripts/groups/components/item_stats.vue
app/assets/javascripts/groups/components/item_stats.vue
+98
-0
app/assets/javascripts/groups/components/item_type_icon.vue
app/assets/javascripts/groups/components/item_type_icon.vue
+34
-0
app/assets/javascripts/groups/constants.js
app/assets/javascripts/groups/constants.js
+35
-0
app/assets/javascripts/groups/groups_filterable_list.js
app/assets/javascripts/groups/groups_filterable_list.js
+16
-9
app/assets/javascripts/groups/index.js
app/assets/javascripts/groups/index.js
+46
-156
app/assets/javascripts/groups/new_group_child.js
app/assets/javascripts/groups/new_group_child.js
+62
-0
app/assets/javascripts/groups/service/groups_service.js
app/assets/javascripts/groups/service/groups_service.js
+1
-1
app/assets/javascripts/groups/store/groups_store.js
app/assets/javascripts/groups/store/groups_store.js
+105
-0
app/assets/javascripts/groups/stores/groups_store.js
app/assets/javascripts/groups/stores/groups_store.js
+0
-167
app/assets/stylesheets/framework/lists.scss
app/assets/stylesheets/framework/lists.scss
+85
-4
app/assets/stylesheets/pages/groups.scss
app/assets/stylesheets/pages/groups.scss
+109
-6
app/helpers/sorting_helper.rb
app/helpers/sorting_helper.rb
+11
-0
app/views/dashboard/_groups_head.html.haml
app/views/dashboard/_groups_head.html.haml
+3
-3
app/views/dashboard/groups/_empty_state.html.haml
app/views/dashboard/groups/_empty_state.html.haml
+0
-7
app/views/dashboard/groups/_groups.html.haml
app/views/dashboard/groups/_groups.html.haml
+1
-8
app/views/dashboard/groups/index.html.haml
app/views/dashboard/groups/index.html.haml
+2
-2
app/views/explore/groups/_groups.html.haml
app/views/explore/groups/_groups.html.haml
+1
-5
app/views/explore/groups/index.html.haml
app/views/explore/groups/index.html.haml
+6
-3
app/views/groups/_children.html.haml
app/views/groups/_children.html.haml
+5
-4
app/views/groups/show.html.haml
app/views/groups/show.html.haml
+30
-7
app/views/shared/groups/_dropdown.html.haml
app/views/shared/groups/_dropdown.html.haml
+18
-15
app/views/shared/groups/_empty_state.html.haml
app/views/shared/groups/_empty_state.html.haml
+7
-0
app/views/shared/groups/_group.html.haml
app/views/shared/groups/_group.html.haml
+1
-1
app/views/shared/groups/_list.html.haml
app/views/shared/groups/_list.html.haml
+1
-1
app/views/shared/groups/_search_form.html.haml
app/views/shared/groups/_search_form.html.haml
+2
-2
app/views/shared/projects/_dropdown.html.haml
app/views/shared/projects/_dropdown.html.haml
+1
-1
spec/features/dashboard/groups_list_spec.rb
spec/features/dashboard/groups_list_spec.rb
+4
-3
spec/features/explore/groups_list_spec.rb
spec/features/explore/groups_list_spec.rb
+7
-6
spec/javascripts/groups/components/app_spec.js
spec/javascripts/groups/components/app_spec.js
+440
-0
spec/javascripts/groups/components/group_folder_spec.js
spec/javascripts/groups/components/group_folder_spec.js
+66
-0
spec/javascripts/groups/components/group_item_spec.js
spec/javascripts/groups/components/group_item_spec.js
+177
-0
spec/javascripts/groups/components/groups_spec.js
spec/javascripts/groups/components/groups_spec.js
+70
-0
spec/javascripts/groups/components/item_actions_spec.js
spec/javascripts/groups/components/item_actions_spec.js
+110
-0
spec/javascripts/groups/components/item_caret_spec.js
spec/javascripts/groups/components/item_caret_spec.js
+40
-0
spec/javascripts/groups/components/item_stats_spec.js
spec/javascripts/groups/components/item_stats_spec.js
+159
-0
spec/javascripts/groups/components/item_type_icon_spec.js
spec/javascripts/groups/components/item_type_icon_spec.js
+54
-0
spec/javascripts/groups/group_item_spec.js
spec/javascripts/groups/group_item_spec.js
+0
-102
spec/javascripts/groups/groups_spec.js
spec/javascripts/groups/groups_spec.js
+0
-99
spec/javascripts/groups/mock_data.js
spec/javascripts/groups/mock_data.js
+368
-102
spec/javascripts/groups/service/groups_service_spec.js
spec/javascripts/groups/service/groups_service_spec.js
+41
-0
spec/javascripts/groups/store/groups_store_spec.js
spec/javascripts/groups/store/groups_store_spec.js
+110
-0
No files found.
app/assets/javascripts/filterable_list.js
View file @
de553961
...
...
@@ -6,10 +6,11 @@ import _ from 'underscore';
*/
export
default
class
FilterableList
{
constructor
(
form
,
filter
,
holder
)
{
constructor
(
form
,
filter
,
holder
,
filterInputField
=
'
filter_groups
'
)
{
this
.
filterForm
=
form
;
this
.
listFilterElement
=
filter
;
this
.
listHolderElement
=
holder
;
this
.
filterInputField
=
filterInputField
;
this
.
isBusy
=
false
;
}
...
...
@@ -32,10 +33,10 @@ export default class FilterableList {
onFilterInput
()
{
const
$form
=
$
(
this
.
filterForm
);
const
queryData
=
{};
const
filterGroupsParam
=
$form
.
find
(
'
[name="filter_groups"]
'
).
val
();
const
filterGroupsParam
=
$form
.
find
(
`[name="
${
this
.
filterInputField
}
"]`
).
val
();
if
(
filterGroupsParam
)
{
queryData
.
filter_groups
=
filterGroupsParam
;
queryData
[
this
.
filterInputField
]
=
filterGroupsParam
;
}
this
.
filterResults
(
queryData
);
...
...
app/assets/javascripts/groups/components/app.vue
0 → 100644
View file @
de553961
<
script
>
/* global Flash */
import
eventHub
from
'
../event_hub
'
;
import
{
getParameterByName
}
from
'
../../lib/utils/common_utils
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
{
COMMON_STR
}
from
'
../constants
'
;
import
groupsComponent
from
'
./groups.vue
'
;
export
default
{
components
:
{
loadingIcon
,
groupsComponent
,
},
props
:
{
store
:
{
type
:
Object
,
required
:
true
,
},
service
:
{
type
:
Object
,
required
:
true
,
},
hideProjects
:
{
type
:
Boolean
,
required
:
true
,
},
},
data
()
{
return
{
isLoading
:
true
,
isSearchEmpty
:
false
,
searchEmptyMessage
:
''
,
};
},
computed
:
{
groups
()
{
return
this
.
store
.
getGroups
();
},
pageInfo
()
{
return
this
.
store
.
getPaginationInfo
();
},
},
methods
:
{
fetchGroups
({
parentId
,
page
,
filterGroupsBy
,
sortBy
,
updatePagination
})
{
return
this
.
service
.
getGroups
(
parentId
,
page
,
filterGroupsBy
,
sortBy
)
.
then
((
res
)
=>
{
if
(
updatePagination
)
{
this
.
updatePagination
(
res
.
headers
);
}
return
res
;
})
.
then
(
res
=>
res
.
json
())
.
catch
(()
=>
{
this
.
isLoading
=
false
;
$
.
scrollTo
(
0
);
Flash
(
COMMON_STR
.
FAILURE
);
});
},
fetchAllGroups
()
{
const
page
=
getParameterByName
(
'
page
'
)
||
null
;
const
sortBy
=
getParameterByName
(
'
sort
'
)
||
null
;
const
filterGroupsBy
=
getParameterByName
(
'
filter
'
)
||
null
;
this
.
isLoading
=
true
;
// eslint-disable-next-line promise/catch-or-return
this
.
fetchGroups
({
page
,
filterGroupsBy
,
sortBy
,
updatePagination
:
true
,
}).
then
((
res
)
=>
{
this
.
isLoading
=
false
;
this
.
updateGroups
(
res
,
Boolean
(
filterGroupsBy
));
});
},
fetchPage
(
page
,
filterGroupsBy
,
sortBy
)
{
this
.
isLoading
=
true
;
// eslint-disable-next-line promise/catch-or-return
this
.
fetchGroups
({
page
,
filterGroupsBy
,
sortBy
,
updatePagination
:
true
,
}).
then
((
res
)
=>
{
this
.
isLoading
=
false
;
$
.
scrollTo
(
0
);
const
currentPath
=
gl
.
utils
.
mergeUrlParams
({
page
},
window
.
location
.
href
);
window
.
history
.
replaceState
({
page
:
currentPath
,
},
document
.
title
,
currentPath
);
this
.
updateGroups
(
res
);
});
},
toggleChildren
(
group
)
{
const
parentGroup
=
group
;
if
(
!
parentGroup
.
isOpen
)
{
if
(
parentGroup
.
children
.
length
===
0
)
{
parentGroup
.
isChildrenLoading
=
true
;
// eslint-disable-next-line promise/catch-or-return
this
.
fetchGroups
({
parentId
:
parentGroup
.
id
,
}).
then
((
res
)
=>
{
this
.
store
.
setGroupChildren
(
parentGroup
,
res
);
}).
catch
(()
=>
{
parentGroup
.
isChildrenLoading
=
false
;
});
}
else
{
parentGroup
.
isOpen
=
true
;
}
}
else
{
parentGroup
.
isOpen
=
false
;
}
},
leaveGroup
(
group
,
parentGroup
)
{
const
targetGroup
=
group
;
targetGroup
.
isBeingRemoved
=
true
;
this
.
service
.
leaveGroup
(
targetGroup
.
leavePath
)
.
then
(
res
=>
res
.
json
())
.
then
((
res
)
=>
{
$
.
scrollTo
(
0
);
this
.
store
.
removeGroup
(
targetGroup
,
parentGroup
);
Flash
(
res
.
notice
,
'
notice
'
);
})
.
catch
((
err
)
=>
{
let
message
=
COMMON_STR
.
FAILURE
;
if
(
err
.
status
===
403
)
{
message
=
COMMON_STR
.
LEAVE_FORBIDDEN
;
}
Flash
(
message
);
targetGroup
.
isBeingRemoved
=
false
;
});
},
updatePagination
(
headers
)
{
this
.
store
.
setPaginationInfo
(
headers
);
},
updateGroups
(
groups
,
fromSearch
)
{
this
.
isSearchEmpty
=
groups
?
groups
.
length
===
0
:
false
;
if
(
fromSearch
)
{
this
.
store
.
setSearchedGroups
(
groups
);
}
else
{
this
.
store
.
setGroups
(
groups
);
}
},
},
created
()
{
this
.
searchEmptyMessage
=
this
.
hideProjects
?
COMMON_STR
.
GROUP_SEARCH_EMPTY
:
COMMON_STR
.
GROUP_PROJECT_SEARCH_EMPTY
;
eventHub
.
$on
(
'
fetchPage
'
,
this
.
fetchPage
);
eventHub
.
$on
(
'
toggleChildren
'
,
this
.
toggleChildren
);
eventHub
.
$on
(
'
leaveGroup
'
,
this
.
leaveGroup
);
eventHub
.
$on
(
'
updatePagination
'
,
this
.
updatePagination
);
eventHub
.
$on
(
'
updateGroups
'
,
this
.
updateGroups
);
},
mounted
()
{
this
.
fetchAllGroups
();
},
beforeDestroy
()
{
eventHub
.
$off
(
'
fetchPage
'
,
this
.
fetchPage
);
eventHub
.
$off
(
'
toggleChildren
'
,
this
.
toggleChildren
);
eventHub
.
$off
(
'
leaveGroup
'
,
this
.
leaveGroup
);
eventHub
.
$off
(
'
updatePagination
'
,
this
.
updatePagination
);
eventHub
.
$off
(
'
updateGroups
'
,
this
.
updateGroups
);
},
};
</
script
>
<
template
>
<div>
<loading-icon
class=
"loading-animation prepend-top-20"
size=
"2"
v-if=
"isLoading"
:label=
"s__('GroupsTree|Loading groups')"
/>
<groups-component
v-if=
"!isLoading"
:groups=
"groups"
:search-empty=
"isSearchEmpty"
:search-empty-message=
"searchEmptyMessage"
:page-info=
"pageInfo"
/>
</div>
</
template
>
app/assets/javascripts/groups/components/group_folder.vue
View file @
de553961
<
script
>
import
{
n__
}
from
'
../../locale
'
;
import
{
MAX_CHILDREN_COUNT
}
from
'
../constants
'
;
export
default
{
props
:
{
groups
:
{
type
:
Object
,
required
:
true
,
},
baseGroup
:
{
parentGroup
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
groups
:
{
type
:
Array
,
required
:
false
,
default
:
()
=>
([]),
},
},
computed
:
{
hasMoreChildren
()
{
return
this
.
parentGroup
.
childrenCount
>
MAX_CHILDREN_COUNT
;
},
moreChildrenStats
()
{
return
n__
(
'
One more item
'
,
'
%d more items
'
,
this
.
parentGroup
.
childrenCount
-
this
.
parentGroup
.
children
.
length
);
},
},
};
</
script
>
...
...
@@ -20,8 +32,20 @@ export default {
v-for=
"(group, index) in groups"
:key=
"index"
:group=
"group"
:base-group=
"baseGroup"
:collection=
"groups"
:parent-group=
"parentGroup"
/>
<li
v-if=
"hasMoreChildren"
class=
"group-row"
>
<a
:href=
"parentGroup.relativePath"
class=
"group-row-contents has-more-items"
>
<i
class=
"fa fa-external-link"
aria-hidden=
"true"
/>
{{
moreChildrenStats
}}
</a>
</li>
</ul>
</
template
>
app/assets/javascripts/groups/components/group_item.vue
View file @
de553961
...
...
@@ -2,49 +2,28 @@
import
identicon
from
'
../../vue_shared/components/identicon.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
itemCaret
from
'
./item_caret.vue
'
;
import
itemTypeIcon
from
'
./item_type_icon.vue
'
;
import
itemStats
from
'
./item_stats.vue
'
;
import
itemActions
from
'
./item_actions.vue
'
;
export
default
{
components
:
{
identicon
,
itemCaret
,
itemTypeIcon
,
itemStats
,
itemActions
,
},
props
:
{
group
:
{
type
:
Object
,
required
:
true
,
},
baseGroup
:
{
parentGroup
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
collection
:
{
group
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
},
methods
:
{
onClickRowGroup
(
e
)
{
e
.
stopPropagation
();
// Skip for buttons
if
(
!
(
e
.
target
.
tagName
===
'
A
'
)
&&
!
(
e
.
target
.
tagName
===
'
I
'
&&
e
.
target
.
parentElement
.
tagName
===
'
A
'
))
{
if
(
this
.
group
.
hasSubgroups
)
{
eventHub
.
$emit
(
'
toggleSubGroups
'
,
this
.
group
);
}
else
{
window
.
location
.
href
=
this
.
group
.
groupPath
;
}
}
},
onLeaveGroup
(
e
)
{
e
.
preventDefault
();
// eslint-disable-next-line no-alert
if
(
confirm
(
`Are you sure you want to leave the "
${
this
.
group
.
fullName
}
" group?`
))
{
this
.
leaveGroup
();
}
},
leaveGroup
()
{
eventHub
.
$emit
(
'
leaveGroup
'
,
this
.
group
,
this
.
collection
);
required
:
true
,
},
},
computed
:
{
...
...
@@ -53,51 +32,33 @@ export default {
},
rowClass
()
{
return
{
'
group-row
'
:
true
,
'
is-open
'
:
this
.
group
.
isOpen
,
'
has-subgroups
'
:
this
.
group
.
hasSubgroups
,
'
no-description
'
:
!
this
.
group
.
description
,
'
has-children
'
:
this
.
hasChildren
,
'
has-description
'
:
this
.
group
.
description
,
'
being-removed
'
:
this
.
group
.
isBeingRemoved
,
};
},
visibilityIcon
()
{
return
{
fa
:
true
,
'
fa-globe
'
:
this
.
group
.
visibility
===
'
public
'
,
'
fa-shield
'
:
this
.
group
.
visibility
===
'
internal
'
,
'
fa-lock
'
:
this
.
group
.
visibility
===
'
private
'
,
};
hasChildren
()
{
return
this
.
group
.
childrenCount
>
0
;
},
fullPath
()
{
let
fullPath
=
''
;
if
(
this
.
group
.
isOrphan
)
{
// check if current group is baseGroup
if
(
Object
.
keys
(
this
.
baseGroup
).
length
>
0
&&
this
.
baseGroup
!==
this
.
group
)
{
// Remove baseGroup prefix from our current group.fullName. e.g:
// baseGroup.fullName: `level1`
// group.fullName: `level1 / level2 / level3`
// Result: `level2 / level3`
const
gfn
=
this
.
group
.
fullName
;
const
bfn
=
this
.
baseGroup
.
fullName
;
const
length
=
bfn
.
length
;
const
start
=
gfn
.
indexOf
(
bfn
);
const
extraPrefixChars
=
3
;
fullPath
=
gfn
.
substr
(
start
+
length
+
extraPrefixChars
);
hasAvatar
()
{
return
this
.
group
.
avatarUrl
!==
null
;
},
isGroup
()
{
return
this
.
group
.
type
===
'
group
'
;
},
},
methods
:
{
onClickRowGroup
(
e
)
{
const
NO_EXPAND_CLS
=
'
no-expand
'
;
if
(
!
(
e
.
target
.
classList
.
contains
(
NO_EXPAND_CLS
)
||
e
.
target
.
parentElement
.
classList
.
contains
(
NO_EXPAND_CLS
)))
{
if
(
this
.
hasChildren
)
{
eventHub
.
$emit
(
'
toggleChildren
'
,
this
.
group
);
}
else
{
fullPath
=
this
.
group
.
fullName
;
gl
.
utils
.
visitUrl
(
this
.
group
.
relativePath
)
;
}
}
else
{
fullPath
=
this
.
group
.
name
;
}
return
fullPath
;
},
hasGroups
()
{
return
Object
.
keys
(
this
.
group
.
subGroups
).
length
>
0
;
},
hasAvatar
()
{
return
this
.
group
.
avatarUrl
&&
this
.
group
.
avatarUrl
.
indexOf
(
'
/assets/no_group_avatar
'
)
===
-
1
;
},
},
};
...
...
@@ -108,98 +69,36 @@ export default {
@
click.stop=
"onClickRowGroup"
:id=
"groupDomId"
:class=
"rowClass"
class=
"group-row"
>
<div
class=
"group-row-contents"
>
<div
class=
"controls"
>
<a
v-if=
"group.canEdit"
class=
"edit-group btn"
:href=
"group.editPath"
>
<i
class=
"fa fa-cogs"
aria-hidden=
"true"
>
</i>
</a>
<a
@
click=
"onLeaveGroup"
:href=
"group.leavePath"
class=
"leave-group btn"
title=
"Leave this group"
>
<i
class=
"fa fa-sign-out"
aria-hidden=
"true"
>
</i>
</a>
</div>
<div
class=
"stats"
>
<span
class=
"number-projects"
>
<i
class=
"fa fa-bookmark"
aria-hidden=
"true"
>
</i>
{{
group
.
numberProjects
}}
</span>
<span
class=
"number-users"
>
<i
class=
"fa fa-users"
aria-hidden=
"true"
>
</i>
{{
group
.
numberUsers
}}
</span>
<span
class=
"group-visibility"
>
<i
:class=
"visibilityIcon"
aria-hidden=
"true"
>
</i>
</span>
</div>
<item-actions
v-if=
"isGroup"
:group=
"group"
:parent-group=
"parentGroup"
/>
<item-stats
:item=
"group"
/>
<div
class=
"folder-toggle-wrap"
>
<span
class=
"folder-caret"
v-if=
"group.hasSubgroups"
>
<i
v-if=
"group.isOpen"
class=
"fa fa-caret-down"
aria-hidden=
"true"
>
</i>
<i
v-if=
"!group.isOpen"
class=
"fa fa-caret-right"
aria-hidden=
"true"
>
</i>
</span>
<span
class=
"folder-icon"
>
<i
v-if=
"group.isOpen"
class=
"fa fa-folder-open"
aria-hidden=
"true"
>
</i>
<i
v-if=
"!group.isOpen"
class=
"fa fa-folder"
aria-hidden=
"true"
>
</i>
</span>
<item-caret
:is-group-open=
"group.isOpen"
/>
<item-type-icon
:item-type=
"group.type"
:is-group-open=
"group.isOpen"
/>
</div>
<div
class=
"avatar-container s40 hidden-xs"
>
class=
"avatar-container s40 hidden-xs"
:class=
"
{ 'content-loading': group.isChildrenLoading }"
>
<a
:href=
"group.groupPath"
>
:href=
"group.relativePath"
class=
"no-expand"
>
<img
v-if=
"hasAvatar"
class=
"avatar s40"
...
...
@@ -215,19 +114,22 @@ export default {
<div
class=
"title"
>
<a
:href=
"group.groupPath"
>
{{
fullPath
}}
</a>
<template
v-if=
"group.permissions.humanGroupAccess"
>
as
<span
class=
"access-type"
>
{{
group
.
permissions
.
humanGroupAccess
}}
</span>
</
template
>
:href=
"group.relativePath"
class=
"no-expand"
>
{{
group
.
fullName
}}
</a>
<span
v-if=
"group.permission"
class=
"access-type"
>
{{
s__
(
'
GroupsTreeRole|as
'
)
}}
{{
group
.
permission
}}
</span>
</div>
<div
class=
"description"
>
{{
group
.
description
}}
</div>
</div>
<group-folder
v-if=
"group.isOpen && has
Groups
"
:
groups=
"group.subGroups
"
:
baseGroup=
"group
"
v-if=
"group.isOpen && has
Children
"
:
parent-group=
"group
"
:
groups=
"group.children
"
/>
</li>
</
template
>
app/assets/javascripts/groups/components/groups.vue
View file @
de553961
...
...
@@ -4,18 +4,26 @@ import eventHub from '../event_hub';
import
{
getParameterByName
}
from
'
../../lib/utils/common_utils
'
;
export
default
{
components
:
{
tablePagination
,
},
props
:
{
groups
:
{
type
:
Object
,
type
:
Array
,
required
:
true
,
},
pageInfo
:
{
type
:
Object
,
required
:
true
,
},
},
components
:
{
tablePagination
,
searchEmpty
:
{
type
:
Boolean
,
required
:
true
,
},
searchEmptyMessage
:
{
type
:
String
,
required
:
true
,
},
},
methods
:
{
change
(
page
)
{
...
...
@@ -29,10 +37,17 @@ export default {
<
template
>
<div
class=
"groups-list-tree-container"
>
<div
v-if=
"searchEmpty"
class=
"has-no-search-results"
>
{{
searchEmptyMessage
}}
</div>
<group-folder
v-if=
"!searchEmpty"
:groups=
"groups"
/>
<table-pagination
v-if=
"!searchEmpty"
:change=
"change"
:pageInfo=
"pageInfo"
/>
...
...
app/assets/javascripts/groups/components/item_actions.vue
0 → 100644
View file @
de553961
<
script
>
import
{
s__
}
from
'
../../locale
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
PopupDialog
from
'
../../vue_shared/components/popup_dialog.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
{
COMMON_STR
}
from
'
../constants
'
;
export
default
{
components
:
{
PopupDialog
,
},
directives
:
{
tooltip
,
},
props
:
{
parentGroup
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
group
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
dialogStatus
:
false
,
};
},
computed
:
{
leaveBtnTitle
()
{
return
COMMON_STR
.
LEAVE_BTN_TITLE
;
},
editBtnTitle
()
{
return
COMMON_STR
.
EDIT_BTN_TITLE
;
},
leaveConfirmationMessage
()
{
return
s__
(
`GroupsTree|Are you sure you want to leave the "
${
this
.
group
.
fullName
}
" group?`
);
},
},
methods
:
{
onLeaveGroup
()
{
this
.
dialogStatus
=
true
;
},
leaveGroup
(
leaveConfirmed
)
{
this
.
dialogStatus
=
false
;
if
(
leaveConfirmed
)
{
eventHub
.
$emit
(
'
leaveGroup
'
,
this
.
group
,
this
.
parentGroup
);
}
},
},
};
</
script
>
<
template
>
<div
class=
"controls"
>
<a
v-tooltip
v-if=
"group.canEdit"
:href=
"group.editPath"
:title=
"editBtnTitle"
:aria-label=
"editBtnTitle"
data-container=
"body"
class=
"edit-group btn no-expand"
>
<i
class=
"fa fa-cogs"
aria-hidden=
"true"
/>
</a>
<a
v-tooltip
v-if=
"group.canLeave"
@
click.prevent=
"onLeaveGroup"
:href=
"group.leavePath"
:title=
"leaveBtnTitle"
:aria-label=
"leaveBtnTitle"
data-container=
"body"
class=
"leave-group btn no-expand"
>
<i
class=
"fa fa-sign-out"
aria-hidden=
"true"
/>
</a>
<popup-dialog
v-show=
"dialogStatus"
:primary-button-label=
"__('Leave')"
kind=
"warning"
:title=
"__('Are you sure?')"
:body=
"leaveConfirmationMessage"
@
submit=
"leaveGroup"
/>
</div>
</
template
>
app/assets/javascripts/groups/components/item_caret.vue
0 → 100644
View file @
de553961
<
script
>
export
default
{
props
:
{
isGroupOpen
:
{
type
:
Boolean
,
required
:
true
,
default
:
false
,
},
},
computed
:
{
iconClass
()
{
return
this
.
isGroupOpen
?
'
fa-caret-down
'
:
'
fa-caret-right
'
;
},
},
};
</
script
>
<
template
>
<span
class=
"folder-caret"
>
<i
:class=
"iconClass"
class=
"fa"
aria-hidden=
"true"
/>
</span>
</
template
>
app/assets/javascripts/groups/components/item_stats.vue
0 → 100644
View file @
de553961
<
script
>
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
{
ITEM_TYPE
,
VISIBILITY_TYPE_ICON
,
GROUP_VISIBILITY_TYPE
,
PROJECT_VISIBILITY_TYPE
}
from
'
../constants
'
;
export
default
{
directives
:
{
tooltip
,
},
props
:
{
item
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
visibilityIcon
()
{
return
VISIBILITY_TYPE_ICON
[
this
.
item
.
visibility
];
},
visibilityTooltip
()
{
if
(
this
.
item
.
type
===
ITEM_TYPE
.
GROUP
)
{
return
GROUP_VISIBILITY_TYPE
[
this
.
item
.
visibility
];
}
return
PROJECT_VISIBILITY_TYPE
[
this
.
item
.
visibility
];
},
isProject
()
{
return
this
.
item
.
type
===
ITEM_TYPE
.
PROJECT
;
},
isGroup
()
{
return
this
.
item
.
type
===
ITEM_TYPE
.
GROUP
;
},
},
};
</
script
>
<
template
>
<div
class=
"stats"
>
<span
v-tooltip
v-if=
"isGroup"
:title=
"s__('Subgroups')"
class=
"number-subgroups"
data-placement=
"top"
data-container=
"body"
>
<i
class=
"fa fa-folder"
aria-hidden=
"true"
/>
{{
item
.
subgroupCount
}}
</span>
<span
v-tooltip
v-if=
"isGroup"
:title=
"s__('Projects')"
class=
"number-projects"
data-placement=
"top"
data-container=
"body"
>
<i
class=
"fa fa-bookmark"
aria-hidden=
"true"
/>
{{
item
.
projectCount
}}
</span>
<span
v-tooltip
v-if=
"isGroup"
:title=
"s__('Members')"
class=
"number-users"
data-placement=
"top"
data-container=
"body"
>
<i
class=
"fa fa-users"
aria-hidden=
"true"
/>
{{
item
.
memberCount
}}
</span>
<span
v-if=
"isProject"
class=
"project-stars"
>
<i
class=
"fa fa-star"
aria-hidden=
"true"
/>
{{
item
.
starCount
}}
</span>
<span
v-tooltip
:title=
"visibilityTooltip"
data-placement=
"left"
data-container=
"body"
class=
"item-visibility"
>
<i
:class=
"visibilityIcon"
class=
"fa"
aria-hidden=
"true"
/>
</span>
</div>
</
template
>
app/assets/javascripts/groups/components/item_type_icon.vue
0 → 100644
View file @
de553961
<
script
>
import
{
ITEM_TYPE
}
from
'
../constants
'
;
export
default
{
props
:
{
itemType
:
{
type
:
String
,
required
:
true
,
},
isGroupOpen
:
{
type
:
Boolean
,
required
:
true
,
default
:
false
,
},
},
computed
:
{
iconClass
()
{
if
(
this
.
itemType
===
ITEM_TYPE
.
GROUP
)
{
return
this
.
isGroupOpen
?
'
fa-folder-open
'
:
'
fa-folder
'
;
}
return
'
fa-bookmark
'
;
},
},
};
</
script
>
<
template
>
<span
class=
"item-type-icon"
>
<i
:class=
"iconClass"
class=
"fa"
aria-hidden=
"true"
/>
</span>
</
template
>
app/assets/javascripts/groups/constants.js
0 → 100644
View file @
de553961
import
{
__
,
s__
}
from
'
../locale
'
;
export
const
MAX_CHILDREN_COUNT
=
20
;
export
const
COMMON_STR
=
{
FAILURE
:
__
(
'
An error occurred. Please try again.
'
),
LEAVE_FORBIDDEN
:
s__
(
'
GroupsTree|Failed to leave the group. Please make sure you are not the only owner.
'
),
LEAVE_BTN_TITLE
:
s__
(
'
GroupsTree|Leave this group
'
),
EDIT_BTN_TITLE
:
s__
(
'
GroupsTree|Edit group
'
),
GROUP_SEARCH_EMPTY
:
s__
(
'
GroupsTree|Sorry, no groups matched your search
'
),
GROUP_PROJECT_SEARCH_EMPTY
:
s__
(
'
GroupsTree|Sorry, no groups or projects matched your search
'
),
};
export
const
ITEM_TYPE
=
{
PROJECT
:
'
project
'
,
GROUP
:
'
group
'
,
};
export
const
GROUP_VISIBILITY_TYPE
=
{
public
:
__
(
'
Public - The group and any public projects can be viewed without any authentication.
'
),
internal
:
__
(
'
Internal - The group and any internal projects can be viewed by any logged in user.
'
),
private
:
__
(
'
Private - The group and its projects can only be viewed by members.
'
),
};
export
const
PROJECT_VISIBILITY_TYPE
=
{
public
:
__
(
'
Public - The project can be accessed without any authentication.
'
),
internal
:
__
(
'
Internal - The project can be accessed by any logged in user.
'
),
private
:
__
(
'
Private - Project access must be granted explicitly to each user.
'
),
};
export
const
VISIBILITY_TYPE_ICON
=
{
public
:
'
fa-globe
'
,
internal
:
'
fa-shield
'
,
private
:
'
fa-lock
'
,
};
app/assets/javascripts/groups/groups_filterable_list.js
View file @
de553961
...
...
@@ -3,12 +3,13 @@ import eventHub from './event_hub';
import
{
getParameterByName
}
from
'
../lib/utils/common_utils
'
;
export
default
class
GroupFilterableList
extends
FilterableList
{
constructor
({
form
,
filter
,
holder
,
filterEndpoint
,
pagePath
})
{
super
(
form
,
filter
,
holder
);
constructor
({
form
,
filter
,
holder
,
filterEndpoint
,
pagePath
,
dropdownSel
,
filterInputField
})
{
super
(
form
,
filter
,
holder
,
filterInputField
);
this
.
form
=
form
;
this
.
filterEndpoint
=
filterEndpoint
;
this
.
pagePath
=
pagePath
;
this
.
$dropdown
=
$
(
'
.js-group-filter-dropdown-wrap
'
);
this
.
filterInputField
=
filterInputField
;
this
.
$dropdown
=
$
(
dropdownSel
);
}
getFilterEndpoint
()
{
...
...
@@ -35,11 +36,11 @@ export default class GroupFilterableList extends FilterableList {
e
.
preventDefault
();
const
$form
=
$
(
this
.
form
);
const
filterGroupsParam
=
$form
.
find
(
'
[name="filter_groups"]
'
).
val
();
const
filterGroupsParam
=
$form
.
find
(
`[name="
${
this
.
filterInputField
}
"]`
).
val
();
const
queryData
=
{};
if
(
filterGroupsParam
)
{
queryData
.
filter_groups
=
filterGroupsParam
;
queryData
[
this
.
filterInputField
]
=
filterGroupsParam
;
}
this
.
filterResults
(
queryData
);
...
...
@@ -47,7 +48,7 @@ export default class GroupFilterableList extends FilterableList {
}
setDefaultFilterOption
()
{
const
defaultOption
=
$
.
trim
(
this
.
$dropdown
.
find
(
'
.dropdown-menu a
:first-child
'
).
text
());
const
defaultOption
=
$
.
trim
(
this
.
$dropdown
.
find
(
'
.dropdown-menu a
'
).
first
(
).
text
());
this
.
$dropdown
.
find
(
'
.dropdown-label
'
).
text
(
defaultOption
);
}
...
...
@@ -65,13 +66,15 @@ export default class GroupFilterableList extends FilterableList {
// Active selected option
this
.
$dropdown
.
find
(
'
.dropdown-label
'
).
text
(
$
.
trim
(
e
.
currentTarget
.
text
));
this
.
$dropdown
.
find
(
'
.dropdown-menu li a
'
).
removeClass
(
'
is-active
'
);
$
(
e
.
target
).
addClass
(
'
is-active
'
);
// Clear current value on search form
this
.
form
.
querySelector
(
'
[name="filter_groups"]
'
).
value
=
''
;
this
.
form
.
querySelector
(
`[name="
${
this
.
filterInputField
}
"]`
).
value
=
''
;
}
onFilterSuccess
(
data
,
xhr
,
queryData
)
{
super
.
onFilterSuccess
(
data
,
xhr
,
queryData
);
const
currentPath
=
this
.
getPagePath
(
queryData
);
const
paginationData
=
{
'
X-Per-Page
'
:
xhr
.
getResponseHeader
(
'
X-Per-Page
'
),
...
...
@@ -82,7 +85,11 @@ export default class GroupFilterableList extends FilterableList {
'
X-Prev-Page
'
:
xhr
.
getResponseHeader
(
'
X-Prev-Page
'
),
};
eventHub
.
$emit
(
'
updateGroups
'
,
data
);
window
.
history
.
replaceState
({
page
:
currentPath
,
},
document
.
title
,
currentPath
);
eventHub
.
$emit
(
'
updateGroups
'
,
data
,
Object
.
prototype
.
hasOwnProperty
.
call
(
queryData
,
this
.
filterInputField
));
eventHub
.
$emit
(
'
updatePagination
'
,
paginationData
);
}
}
app/assets/javascripts/groups/index.js
View file @
de553961
/* global Flash */
import
Vue
from
'
vue
'
;
import
Translate
from
'
../vue_shared/translate
'
;
import
GroupFilterableList
from
'
./groups_filterable_list
'
;
import
GroupsComponent
from
'
./components/groups.vue
'
;
import
GroupFolder
from
'
./components/group_folder.vue
'
;
import
GroupItem
from
'
./components/group_item.vue
'
;
import
GroupsStore
from
'
./stores/groups_store
'
;
import
GroupsService
from
'
./services/groups_service
'
;
import
eventHub
from
'
./event_hub
'
;
import
{
getParameterByName
}
from
'
../lib/utils/common_utils
'
;
import
NewGroupChild
from
'
./new_group_child
'
;
import
GroupsStore
from
'
./store/groups_store
'
;
import
GroupsService
from
'
./service/groups_service
'
;
import
groupsApp
from
'
./components/app.vue
'
;
import
groupFolderComponent
from
'
./components/group_folder.vue
'
;
import
groupItemComponent
from
'
./components/group_item.vue
'
;
Vue
.
use
(
Translate
);
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
const
el
=
document
.
getElementById
(
'
dashboard-group-app
'
);
const
el
=
document
.
getElementById
(
'
js-groups-tree
'
);
const
newGroupChildWrapper
=
document
.
querySelector
(
'
.js-new-project-subgroup
'
);
// Don't do anything if element doesn't exist (No groups)
// This is for when the user enters directly to the page via URL
...
...
@@ -19,176 +24,61 @@ document.addEventListener('DOMContentLoaded', () => {
return
;
}
Vue
.
component
(
'
groups-component
'
,
GroupsComponent
);
Vue
.
component
(
'
group-folder
'
,
GroupFolder
);
Vue
.
component
(
'
group-item
'
,
GroupItem
);
Vue
.
component
(
'
group-folder
'
,
groupFolderComponent
);
Vue
.
component
(
'
group-item
'
,
groupItemComponent
);
if
(
newGroupChildWrapper
)
{
// eslint-disable-next-line no-new
new
NewGroupChild
(
newGroupChildWrapper
);
}
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
groupsApp
,
},
data
()
{
this
.
store
=
new
GroupsStore
();
this
.
service
=
new
GroupsService
(
el
.
dataset
.
endpoint
);
const
dataset
=
this
.
$options
.
el
.
dataset
;
const
hideProjects
=
dataset
.
hideProjects
===
'
true
'
;
const
store
=
new
GroupsStore
(
hideProjects
);
const
service
=
new
GroupsService
(
dataset
.
endpoint
);
return
{
store
:
this
.
store
,
isLoading
:
tru
e
,
state
:
this
.
store
.
state
,
store
,
servic
e
,
hideProjects
,
loading
:
true
,
};
},
computed
:
{
isEmpty
()
{
return
Object
.
keys
(
this
.
state
.
groups
).
length
===
0
;
},
},
methods
:
{
fetchGroups
(
parentGroup
)
{
let
parentId
=
null
;
let
getGroups
=
null
;
let
page
=
null
;
let
sort
=
null
;
let
pageParam
=
null
;
let
sortParam
=
null
;
let
filterGroups
=
null
;
let
filterGroupsParam
=
null
;
if
(
parentGroup
)
{
parentId
=
parentGroup
.
id
;
}
else
{
this
.
isLoading
=
true
;
}
pageParam
=
getParameterByName
(
'
page
'
);
if
(
pageParam
)
{
page
=
pageParam
;
}
filterGroupsParam
=
getParameterByName
(
'
filter_groups
'
);
if
(
filterGroupsParam
)
{
filterGroups
=
filterGroupsParam
;
}
sortParam
=
getParameterByName
(
'
sort
'
);
if
(
sortParam
)
{
sort
=
sortParam
;
}
getGroups
=
this
.
service
.
getGroups
(
parentId
,
page
,
filterGroups
,
sort
);
getGroups
.
then
(
response
=>
response
.
json
())
.
then
((
response
)
=>
{
this
.
isLoading
=
false
;
this
.
updateGroups
(
response
,
parentGroup
);
})
.
catch
(
this
.
handleErrorResponse
);
return
getGroups
;
},
fetchPage
(
page
,
filterGroups
,
sort
)
{
this
.
isLoading
=
true
;
return
this
.
service
.
getGroups
(
null
,
page
,
filterGroups
,
sort
)
.
then
((
response
)
=>
{
this
.
isLoading
=
false
;
$
.
scrollTo
(
0
);
const
currentPath
=
gl
.
utils
.
mergeUrlParams
({
page
},
window
.
location
.
href
);
window
.
history
.
replaceState
({
page
:
currentPath
,
},
document
.
title
,
currentPath
);
return
response
.
json
().
then
((
data
)
=>
{
this
.
updateGroups
(
data
);
this
.
updatePagination
(
response
.
headers
);
});
})
.
catch
(
this
.
handleErrorResponse
);
},
toggleSubGroups
(
parentGroup
=
null
)
{
if
(
!
parentGroup
.
isOpen
)
{
this
.
store
.
resetGroups
(
parentGroup
);
this
.
fetchGroups
(
parentGroup
);
}
this
.
store
.
toggleSubGroups
(
parentGroup
);
},
leaveGroup
(
group
,
collection
)
{
this
.
service
.
leaveGroup
(
group
.
leavePath
)
.
then
(
resp
=>
resp
.
json
())
.
then
((
response
)
=>
{
$
.
scrollTo
(
0
);
this
.
store
.
removeGroup
(
group
,
collection
);
// eslint-disable-next-line no-new
new
Flash
(
response
.
notice
,
'
notice
'
);
})
.
catch
((
error
)
=>
{
let
message
=
'
An error occurred. Please try again.
'
;
if
(
error
.
status
===
403
)
{
message
=
'
Failed to leave the group. Please make sure you are not the only owner
'
;
}
// eslint-disable-next-line no-new
new
Flash
(
message
);
});
},
updateGroups
(
groups
,
parentGroup
)
{
this
.
store
.
setGroups
(
groups
,
parentGroup
);
},
updatePagination
(
headers
)
{
this
.
store
.
storePagination
(
headers
);
},
handleErrorResponse
()
{
this
.
isLoading
=
false
;
$
.
scrollTo
(
0
);
// eslint-disable-next-line no-new
new
Flash
(
'
An error occurred. Please try again.
'
);
},
},
created
()
{
eventHub
.
$on
(
'
fetchPage
'
,
this
.
fetchPage
);
eventHub
.
$on
(
'
toggleSubGroups
'
,
this
.
toggleSubGroups
);
eventHub
.
$on
(
'
leaveGroup
'
,
this
.
leaveGroup
);
eventHub
.
$on
(
'
updateGroups
'
,
this
.
updateGroups
);
eventHub
.
$on
(
'
updatePagination
'
,
this
.
updatePagination
);
},
beforeMount
()
{
const
dataset
=
this
.
$options
.
el
.
dataset
;
let
groupFilterList
=
null
;
const
form
=
document
.
querySelector
(
'
form#group-filter-form
'
);
const
filter
=
document
.
querySelector
(
'
.js-groups-list-filter
'
);
const
holder
=
document
.
querySelector
(
'
.js-groups-list-holder
'
);
const
form
=
document
.
querySelector
(
dataset
.
formSel
);
const
filter
=
document
.
querySelector
(
dataset
.
filterSel
);
const
holder
=
document
.
querySelector
(
dataset
.
holderSel
);
const
opts
=
{
form
,
filter
,
holder
,
filterEndpoint
:
el
.
dataset
.
endpoint
,
pagePath
:
el
.
dataset
.
path
,
filterEndpoint
:
dataset
.
endpoint
,
pagePath
:
dataset
.
path
,
dropdownSel
:
dataset
.
dropdownSel
,
filterInputField
:
'
filter
'
,
};
groupFilterList
=
new
GroupFilterableList
(
opts
);
groupFilterList
.
initSearch
();
},
mounted
()
{
this
.
fetchGroups
()
.
then
((
response
)
=>
{
this
.
updatePagination
(
response
.
headers
);
this
.
isLoading
=
false
;
})
.
catch
(
this
.
handleErrorResponse
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'
fetchPage
'
,
this
.
fetchPage
);
eventHub
.
$off
(
'
toggleSubGroups
'
,
this
.
toggleSubGroups
);
eventHub
.
$off
(
'
leaveGroup
'
,
this
.
leaveGroup
);
eventHub
.
$off
(
'
updateGroups
'
,
this
.
updateGroups
);
eventHub
.
$off
(
'
updatePagination
'
,
this
.
updatePagination
);
render
(
createElement
)
{
return
createElement
(
'
groups-app
'
,
{
props
:
{
store
:
this
.
store
,
service
:
this
.
service
,
hideProjects
:
this
.
hideProjects
,
},
});
},
});
});
app/assets/javascripts/groups/new_group_child.js
0 → 100644
View file @
de553961
import
DropLab
from
'
../droplab/drop_lab
'
;
import
ISetter
from
'
../droplab/plugins/input_setter
'
;
const
InputSetter
=
Object
.
assign
({},
ISetter
);
const
NEW_PROJECT
=
'
new-project
'
;
const
NEW_SUBGROUP
=
'
new-subgroup
'
;
export
default
class
NewGroupChild
{
constructor
(
buttonWrapper
)
{
this
.
buttonWrapper
=
buttonWrapper
;
this
.
newGroupChildButton
=
this
.
buttonWrapper
.
querySelector
(
'
.js-new-group-child
'
);
this
.
dropdownToggle
=
this
.
buttonWrapper
.
querySelector
(
'
.js-dropdown-toggle
'
);
this
.
dropdownList
=
this
.
buttonWrapper
.
querySelector
(
'
.dropdown-menu
'
);
this
.
newGroupPath
=
this
.
buttonWrapper
.
dataset
.
projectPath
;
this
.
subgroupPath
=
this
.
buttonWrapper
.
dataset
.
subgroupPath
;
this
.
init
();
}
init
()
{
this
.
initDroplab
();
this
.
bindEvents
();
}
initDroplab
()
{
this
.
droplab
=
new
DropLab
();
this
.
droplab
.
init
(
this
.
dropdownToggle
,
this
.
dropdownList
,
[
InputSetter
],
this
.
getDroplabConfig
(),
);
}
getDroplabConfig
()
{
return
{
InputSetter
:
[{
input
:
this
.
newGroupChildButton
,
valueAttribute
:
'
data-value
'
,
inputAttribute
:
'
data-action
'
,
},
{
input
:
this
.
newGroupChildButton
,
valueAttribute
:
'
data-text
'
,
}],
};
}
bindEvents
()
{
this
.
newGroupChildButton
.
addEventListener
(
'
click
'
,
this
.
onClickNewGroupChildButton
.
bind
(
this
));
}
onClickNewGroupChildButton
(
e
)
{
if
(
e
.
target
.
dataset
.
action
===
NEW_PROJECT
)
{
gl
.
utils
.
visitUrl
(
this
.
newGroupPath
);
}
else
if
(
e
.
target
.
dataset
.
action
===
NEW_SUBGROUP
)
{
gl
.
utils
.
visitUrl
(
this
.
subgroupPath
);
}
}
}
app/assets/javascripts/groups/service
s
/groups_service.js
→
app/assets/javascripts/groups/service/groups_service.js
View file @
de553961
...
...
@@ -20,7 +20,7 @@ export default class GroupsService {
}
if
(
filterGroups
)
{
data
.
filter
_groups
=
filterGroups
;
data
.
filter
=
filterGroups
;
}
if
(
sort
)
{
...
...
app/assets/javascripts/groups/store/groups_store.js
0 → 100644
View file @
de553961
import
{
normalizeHeaders
,
parseIntPagination
}
from
'
../../lib/utils/common_utils
'
;
export
default
class
GroupsStore
{
constructor
(
hideProjects
)
{
this
.
state
=
{};
this
.
state
.
groups
=
[];
this
.
state
.
pageInfo
=
{};
this
.
hideProjects
=
hideProjects
;
}
setGroups
(
rawGroups
)
{
if
(
rawGroups
&&
rawGroups
.
length
)
{
this
.
state
.
groups
=
rawGroups
.
map
(
rawGroup
=>
this
.
formatGroupItem
(
rawGroup
));
}
else
{
this
.
state
.
groups
=
[];
}
}
setSearchedGroups
(
rawGroups
)
{
const
formatGroups
=
groups
=>
groups
.
map
((
group
)
=>
{
const
formattedGroup
=
this
.
formatGroupItem
(
group
);
if
(
formattedGroup
.
children
&&
formattedGroup
.
children
.
length
)
{
formattedGroup
.
children
=
formatGroups
(
formattedGroup
.
children
);
}
return
formattedGroup
;
});
if
(
rawGroups
&&
rawGroups
.
length
)
{
this
.
state
.
groups
=
formatGroups
(
rawGroups
);
}
else
{
this
.
state
.
groups
=
[];
}
}
setGroupChildren
(
parentGroup
,
children
)
{
const
updatedParentGroup
=
parentGroup
;
updatedParentGroup
.
children
=
children
.
map
(
rawChild
=>
this
.
formatGroupItem
(
rawChild
));
updatedParentGroup
.
isOpen
=
true
;
updatedParentGroup
.
isChildrenLoading
=
false
;
}
getGroups
()
{
return
this
.
state
.
groups
;
}
setPaginationInfo
(
pagination
=
{})
{
let
paginationInfo
;
if
(
Object
.
keys
(
pagination
).
length
)
{
const
normalizedHeaders
=
normalizeHeaders
(
pagination
);
paginationInfo
=
parseIntPagination
(
normalizedHeaders
);
}
else
{
paginationInfo
=
pagination
;
}
this
.
state
.
pageInfo
=
paginationInfo
;
}
getPaginationInfo
()
{
return
this
.
state
.
pageInfo
;
}
formatGroupItem
(
rawGroupItem
)
{
const
groupChildren
=
rawGroupItem
.
children
||
[];
const
groupIsOpen
=
(
groupChildren
.
length
>
0
)
||
false
;
const
childrenCount
=
this
.
hideProjects
?
rawGroupItem
.
subgroup_count
:
rawGroupItem
.
children_count
;
return
{
id
:
rawGroupItem
.
id
,
name
:
rawGroupItem
.
name
,
fullName
:
rawGroupItem
.
full_name
,
description
:
rawGroupItem
.
description
,
visibility
:
rawGroupItem
.
visibility
,
avatarUrl
:
rawGroupItem
.
avatar_url
,
relativePath
:
rawGroupItem
.
relative_path
,
editPath
:
rawGroupItem
.
edit_path
,
leavePath
:
rawGroupItem
.
leave_path
,
canEdit
:
rawGroupItem
.
can_edit
,
canLeave
:
rawGroupItem
.
can_leave
,
type
:
rawGroupItem
.
type
,
permission
:
rawGroupItem
.
permission
,
children
:
groupChildren
,
isOpen
:
groupIsOpen
,
isChildrenLoading
:
false
,
isBeingRemoved
:
false
,
parentId
:
rawGroupItem
.
parent_id
,
childrenCount
,
projectCount
:
rawGroupItem
.
project_count
,
subgroupCount
:
rawGroupItem
.
subgroup_count
,
memberCount
:
rawGroupItem
.
number_users_with_delimiter
,
starCount
:
rawGroupItem
.
star_count
,
};
}
removeGroup
(
group
,
parentGroup
)
{
const
updatedParentGroup
=
parentGroup
;
if
(
updatedParentGroup
.
children
&&
updatedParentGroup
.
children
.
length
)
{
updatedParentGroup
.
children
=
parentGroup
.
children
.
filter
(
child
=>
group
.
id
!==
child
.
id
);
}
else
{
this
.
state
.
groups
=
this
.
state
.
groups
.
filter
(
child
=>
group
.
id
!==
child
.
id
);
}
}
}
app/assets/javascripts/groups/stores/groups_store.js
deleted
100644 → 0
View file @
67815272
import
Vue
from
'
vue
'
;
import
{
parseIntPagination
,
normalizeHeaders
}
from
'
../../lib/utils/common_utils
'
;
export
default
class
GroupsStore
{
constructor
()
{
this
.
state
=
{};
this
.
state
.
groups
=
{};
this
.
state
.
pageInfo
=
{};
}
setGroups
(
rawGroups
,
parent
)
{
const
parentGroup
=
parent
;
const
tree
=
this
.
buildTree
(
rawGroups
,
parentGroup
);
if
(
parentGroup
)
{
parentGroup
.
subGroups
=
tree
;
}
else
{
this
.
state
.
groups
=
tree
;
}
return
tree
;
}
// eslint-disable-next-line class-methods-use-this
resetGroups
(
parent
)
{
const
parentGroup
=
parent
;
parentGroup
.
subGroups
=
{};
}
storePagination
(
pagination
=
{})
{
let
paginationInfo
;
if
(
Object
.
keys
(
pagination
).
length
)
{
const
normalizedHeaders
=
normalizeHeaders
(
pagination
);
paginationInfo
=
parseIntPagination
(
normalizedHeaders
);
}
else
{
paginationInfo
=
pagination
;
}
this
.
state
.
pageInfo
=
paginationInfo
;
}
buildTree
(
rawGroups
,
parentGroup
)
{
const
groups
=
this
.
decorateGroups
(
rawGroups
);
const
tree
=
{};
const
mappedGroups
=
{};
const
orphans
=
[];
// Map groups to an object
groups
.
map
((
group
)
=>
{
mappedGroups
[
`id
${
group
.
id
}
`
]
=
group
;
mappedGroups
[
`id
${
group
.
id
}
`
].
subGroups
=
{};
return
group
;
});
Object
.
keys
(
mappedGroups
).
map
((
key
)
=>
{
const
currentGroup
=
mappedGroups
[
key
];
if
(
currentGroup
.
parentId
)
{
// If the group is not at the root level, add it to its parent array of subGroups.
const
findParentGroup
=
mappedGroups
[
`id
${
currentGroup
.
parentId
}
`
];
if
(
findParentGroup
)
{
mappedGroups
[
`id
${
currentGroup
.
parentId
}
`
].
subGroups
[
`id
${
currentGroup
.
id
}
`
]
=
currentGroup
;
mappedGroups
[
`id
${
currentGroup
.
parentId
}
`
].
isOpen
=
true
;
// Expand group if it has subgroups
}
else
if
(
parentGroup
&&
parentGroup
.
id
===
currentGroup
.
parentId
)
{
tree
[
`id
${
currentGroup
.
id
}
`
]
=
currentGroup
;
}
else
{
// No parent found. We save it for later processing
orphans
.
push
(
currentGroup
);
// Add to tree to preserve original order
tree
[
`id
${
currentGroup
.
id
}
`
]
=
currentGroup
;
}
}
else
{
// If the group is at the top level, add it to first level elements array.
tree
[
`id
${
currentGroup
.
id
}
`
]
=
currentGroup
;
}
return
key
;
});
if
(
orphans
.
length
)
{
orphans
.
map
((
orphan
)
=>
{
let
found
=
false
;
const
currentOrphan
=
orphan
;
Object
.
keys
(
tree
).
map
((
key
)
=>
{
const
group
=
tree
[
key
];
if
(
group
&&
currentOrphan
.
fullPath
.
lastIndexOf
(
group
.
fullPath
)
===
0
&&
// Make sure the currently selected orphan is not the same as the group
// we are checking here otherwise it will end up in an infinite loop
currentOrphan
.
id
!==
group
.
id
)
{
group
.
subGroups
[
currentOrphan
.
id
]
=
currentOrphan
;
group
.
isOpen
=
true
;
currentOrphan
.
isOrphan
=
true
;
found
=
true
;
// Delete if group was put at the top level. If not the group will be displayed twice.
if
(
tree
[
`id
${
currentOrphan
.
id
}
`
])
{
delete
tree
[
`id
${
currentOrphan
.
id
}
`
];
}
}
return
key
;
});
if
(
!
found
)
{
currentOrphan
.
isOrphan
=
true
;
tree
[
`id
${
currentOrphan
.
id
}
`
]
=
currentOrphan
;
}
return
orphan
;
});
}
return
tree
;
}
decorateGroups
(
rawGroups
)
{
this
.
groups
=
rawGroups
.
map
(
this
.
decorateGroup
);
return
this
.
groups
;
}
// eslint-disable-next-line class-methods-use-this
decorateGroup
(
rawGroup
)
{
return
{
id
:
rawGroup
.
id
,
fullName
:
rawGroup
.
full_name
,
fullPath
:
rawGroup
.
full_path
,
avatarUrl
:
rawGroup
.
avatar_url
,
name
:
rawGroup
.
name
,
hasSubgroups
:
rawGroup
.
has_subgroups
,
canEdit
:
rawGroup
.
can_edit
,
description
:
rawGroup
.
description
,
webUrl
:
rawGroup
.
web_url
,
groupPath
:
rawGroup
.
group_path
,
parentId
:
rawGroup
.
parent_id
,
visibility
:
rawGroup
.
visibility
,
leavePath
:
rawGroup
.
leave_path
,
editPath
:
rawGroup
.
edit_path
,
isOpen
:
false
,
isOrphan
:
false
,
numberProjects
:
rawGroup
.
number_projects_with_delimiter
,
numberUsers
:
rawGroup
.
number_users_with_delimiter
,
permissions
:
{
humanGroupAccess
:
rawGroup
.
permissions
.
human_group_access
,
},
subGroups
:
{},
};
}
// eslint-disable-next-line class-methods-use-this
removeGroup
(
group
,
collection
)
{
Vue
.
delete
(
collection
,
`id
${
group
.
id
}
`
);
}
// eslint-disable-next-line class-methods-use-this
toggleSubGroups
(
toggleGroup
)
{
const
group
=
toggleGroup
;
group
.
isOpen
=
!
group
.
isOpen
;
return
group
;
}
}
app/assets/stylesheets/framework/lists.scss
View file @
de553961
...
...
@@ -281,6 +281,57 @@ ul.indent-list {
// Specific styles for tree list
@keyframes
spin-avatar
{
from
{
transform
:
rotate
(
0deg
);
}
to
{
transform
:
rotate
(
360deg
);
}
}
.groups-list-tree-container
{
.has-no-search-results
{
text-align
:
center
;
padding
:
$gl-padding
;
font-style
:
italic
;
color
:
$well-light-text-color
;
}
>
.group-list-tree
>
.group-row.has-children
:first-child
{
border-top
:
none
;
}
}
.group-list-tree
.avatar-container.content-loading
{
position
:
relative
;
>
a
,
>
a
.avatar
{
height
:
100%
;
border-radius
:
50%
;
}
>
a
{
padding
:
2px
;
}
>
a
.avatar
{
border
:
2px
solid
$white-normal
;
&
.identicon
{
line-height
:
30px
;
}
}
&
:
:
after
{
content
:
""
;
position
:
absolute
;
height
:
100%
;
width
:
100%
;
background-color
:
transparent
;
border
:
2px
outset
$kdb-border
;
border-radius
:
50%
;
animation
:
spin-avatar
3s
infinite
linear
;
}
}
.group-list-tree
{
.folder-toggle-wrap
{
float
:
left
;
...
...
@@ -293,7 +344,7 @@ ul.indent-list {
}
.folder-caret
,
.
folder
-icon
{
.
item-type
-icon
{
display
:
inline-block
;
}
...
...
@@ -301,11 +352,11 @@ ul.indent-list {
width
:
15px
;
}
.
folder
-icon
{
.
item-type
-icon
{
width
:
20px
;
}
>
.group-row
:not
(
.has-
subgroups
)
{
>
.group-row
:not
(
.has-
children
)
{
.folder-caret
.fa
{
opacity
:
0
;
}
...
...
@@ -351,12 +402,23 @@ ul.indent-list {
top
:
30px
;
bottom
:
0
;
}
&
.being-removed
{
opacity
:
0
.5
;
}
}
}
.group-row
{
padding
:
0
;
border
:
none
;
&
.has-children
{
border-top
:
none
;
}
&
:first-child
{
border-top
:
1px
solid
$white-normal
;
}
&
:last-of-type
{
.group-row-contents
:not
(
:hover
)
{
...
...
@@ -379,6 +441,25 @@ ul.indent-list {
.avatar-container
>
a
{
width
:
100%
;
}
&
.has-more-items
{
display
:
block
;
padding
:
20px
10px
;
}
}
}
ul
.group-list-tree
{
li
.group-row
{
&
.has-description
{
.title
{
line-height
:
inherit
;
}
}
.title
{
line-height
:
$list-text-height
;
}
}
}
...
...
app/assets/stylesheets/pages/groups.scss
View file @
de553961
...
...
@@ -26,14 +26,117 @@
}
}
.groups-header
{
@media
(
min-width
:
$screen-sm-min
)
{
.nav-links
{
width
:
35%
;
.group-nav-container
.nav-controls
{
display
:
flex
;
align-items
:
flex-start
;
padding
:
$gl-padding-top
0
;
border-bottom
:
1px
solid
$border-color
;
.group-filter-form
{
flex
:
1
;
}
.dropdown-menu-align-right
{
margin-top
:
0
;
}
.new-project-subgroup
{
.dropdown-primary
{
min-width
:
115px
;
}
.dropdown-toggle
{
.dropdown-btn-icon
{
pointer-events
:
none
;
color
:
inherit
;
margin-left
:
0
;
}
}
.nav-controls
{
width
:
65%
;
.dropdown-menu
{
min-width
:
280px
;
margin-top
:
2px
;
}
li
:not
(
.divider
)
{
padding
:
0
;
&
.droplab-item-selected
{
.icon-container
{
.list-item-checkmark
{
visibility
:
visible
;
}
}
}
.menu-item
{
padding
:
8px
4px
;
&
:hover
{
background-color
:
$gray-darker
;
color
:
$theme-gray-900
;
}
}
.icon-container
{
float
:
left
;
padding-left
:
6px
;
.list-item-checkmark
{
visibility
:
hidden
;
}
}
.description
{
font-size
:
14px
;
strong
{
display
:
block
;
font-weight
:
$gl-font-weight-bold
;
}
}
}
}
@media
(
max-width
:
$screen-sm-max
)
{
&
,
.dropdown
,
.dropdown
.dropdown-toggle
,
.btn-new
{
display
:
block
;
}
.group-filter-form
,
.dropdown
{
margin-bottom
:
10px
;
margin-right
:
0
;
}
.group-filter-form
,
.dropdown
.dropdown-toggle
,
.btn-new
{
width
:
100%
;
}
.dropdown
.dropdown-toggle
.fa-chevron-down
{
position
:
absolute
;
top
:
11px
;
right
:
8px
;
}
.new-project-subgroup
{
display
:
flex
;
align-items
:
flex-start
;
.dropdown-primary
{
flex
:
1
;
}
.dropdown-menu
{
width
:
100%
;
max-width
:
inherit
;
min-width
:
inherit
;
}
}
}
}
...
...
app/helpers/sorting_helper.rb
View file @
de553961
...
...
@@ -42,6 +42,17 @@ module SortingHelper
options
end
def
groups_sort_options_hash
options
=
{
sort_value_recently_created
=>
sort_title_recently_created
,
sort_value_oldest_created
=>
sort_title_oldest_created
,
sort_value_recently_updated
=>
sort_title_recently_updated
,
sort_value_oldest_updated
=>
sort_title_oldest_updated
}
options
end
def
member_sort_options_hash
{
sort_value_access_level_asc
=>
sort_title_access_level_asc
,
...
...
app/views/dashboard/_groups_head.html.haml
View file @
de553961
.top-area
%ul
.nav-links
=
nav_link
(
page:
dashboard_groups_path
)
do
=
link_to
dashboard_groups_path
,
title:
'Your groups'
do
=
link_to
dashboard_groups_path
,
title:
_
(
"Your groups"
)
do
Your groups
=
nav_link
(
page:
explore_groups_path
)
do
=
link_to
explore_groups_path
,
title:
'Explore public groups'
do
=
link_to
explore_groups_path
,
title:
_
(
"Explore public groups"
)
do
Explore public groups
.nav-controls
=
render
'shared/groups/search_form'
=
render
'shared/groups/dropdown'
-
if
current_user
.
can_create_group?
=
link_to
"New group"
,
new_group_path
,
class:
"btn btn-new"
=
link_to
_
(
"New group"
)
,
new_group_path
,
class:
"btn btn-new"
app/views/dashboard/groups/_empty_state.html.haml
deleted
100644 → 0
View file @
67815272
.groups-empty-state
=
custom_icon
(
"icon_empty_groups"
)
.text-content
%h4
A group is a collection of several projects.
%p
If you organize your projects under a group, it works like a folder.
%p
You can manage your group member’s permissions and access to each project in the group.
app/views/dashboard/groups/_groups.html.haml
View file @
de553961
.js-groups-list-holder
#dashboard-group-app
{
data:
{
endpoint:
dashboard_groups_path
(
format: :json
),
path:
dashboard_groups_path
}
}
.groups-list-loading
=
icon
(
'spinner spin'
,
'v-show'
=>
'isLoading'
)
%template
{
'v-if'
=>
'!isLoading && isEmpty'
}
%div
{
'v-cloak'
=>
true
}
=
render
'empty_state'
%template
{
'v-else-if'
=>
'!isLoading && !isEmpty'
}
%groups-component
{
':groups'
=>
'state.groups'
,
':page-info'
=>
'state.pageInfo'
}
#js-groups-tree
{
data:
{
hide_projects:
'true'
,
endpoint:
dashboard_groups_path
(
format: :json
),
path:
dashboard_groups_path
,
form_sel:
'form#group-filter-form'
,
filter_sel:
'.js-groups-list-filter'
,
holder_sel:
'.js-groups-list-holder'
,
dropdown_sel:
'.js-group-filter-dropdown-wrap'
}
}
app/views/dashboard/groups/index.html.haml
View file @
de553961
...
...
@@ -6,7 +6,7 @@
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'groups'
-
if
@groups
.
empty?
=
render
'empty_state'
-
if
params
[
:filter
].
blank?
&&
@groups
.
empty?
=
render
'
shared/groups/
empty_state'
-
else
=
render
'groups'
app/views/explore/groups/_groups.html.haml
View file @
de553961
.js-groups-list-holder
%ul
.content-list
-
@groups
.
each
do
|
group
|
=
render
'shared/groups/group'
,
group:
group
=
paginate
@groups
,
theme:
'gitlab'
#js-groups-tree
{
data:
{
hide_projects:
'true'
,
endpoint:
explore_groups_path
(
format: :json
),
path:
explore_groups_path
,
form_sel:
'form#group-filter-form'
,
filter_sel:
'.js-groups-list-filter'
,
holder_sel:
'.js-groups-list-holder'
,
dropdown_sel:
'.js-group-filter-dropdown-wrap'
}
}
app/views/explore/groups/index.html.haml
View file @
de553961
...
...
@@ -2,6 +2,9 @@
-
page_title
"Groups"
-
header_title
"Groups"
,
dashboard_groups_path
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'groups'
-
if
current_user
=
render
'dashboard/groups_head'
-
else
...
...
@@ -17,7 +20,7 @@
%p
Below you will find all the groups that are public.
%p
You can easily contribute to them by requesting to join these groups.
-
if
@groups
.
present?
=
render
'groups'
-
else
-
if
params
[
:filter
].
blank?
&&
@groups
.
empty?
.nothing-here-block
No public groups
-
else
=
render
'groups'
app/views/groups/_children.html.haml
View file @
de553961
-
if
children
.
any?
render children here
-
else
.nothing-here-block
No children found
=
webpack_bundle_tag
'common_vue'
=
webpack_bundle_tag
'groups'
.js-groups-list-holder
#js-groups-tree
{
data:
{
hide_projects:
'false'
,
group_id:
group
.
id
,
endpoint:
group_children_path
(
group
,
format: :json
),
path:
group_path
(
group
),
form_sel:
'form#group-filter-form'
,
filter_sel:
'.js-groups-list-filter'
,
holder_sel:
'.js-groups-list-holder'
,
dropdown_sel:
'.js-group-filter-dropdown-wrap'
}
}
app/views/groups/show.html.haml
View file @
de553961
...
...
@@ -7,12 +7,35 @@
=
render
'groups/home_panel'
.groups-header
{
class:
container_class
}
.
top-area
.nav-controls
=
render
'shared/projects/search_form'
=
render
'shared/projects/dropdown'
.
group-nav-container
.nav-controls
.clearfix
=
render
"shared/groups/search_form"
=
render
"shared/groups/dropdown"
-
if
can?
current_user
,
:create_projects
,
@group
=
link_to
new_project_path
(
namespace_id:
@group
.
id
),
class:
'btn btn-new pull-right'
do
New Project
-
new_project_label
=
_
(
"New project"
)
-
new_subgroup_label
=
_
(
"New subgroup"
)
.btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup
{
data:
{
project_path:
new_project_path
(
namespace_id:
@group
.
id
),
subgroup_path:
new_group_path
(
parent_id:
@group
.
id
)
}
}
%input
.btn.btn-success.dropdown-primary.js-new-group-child
{
type:
"button"
,
value:
new_project_label
,
data:
{
action:
"new-project"
}
}
%button
.btn.btn-success.dropdown-toggle.js-dropdown-toggle
{
type:
"button"
,
data:
{
"dropdown-trigger"
=>
"#new-project-or-subgroup-dropdown"
}
}
=
icon
(
"caret-down"
,
class:
"dropdown-btn-icon"
)
%ul
#new-project-or-subgroup-dropdown
.dropdown-menu.dropdown-menu-align-right
{
data:
{
dropdown:
true
}
}
%li
.droplab-item-selected
{
role:
"button"
,
data:
{
value:
"new-project"
,
text:
new_project_label
}
}
.menu-item
.icon-container
=
icon
(
"check"
,
class:
"list-item-checkmark"
)
.description
%strong
=
new_project_label
%span
=
s_
(
"GroupsTree|Create project under this group."
)
%li
.divider.droplap-item-ignore
%li
{
role:
"button"
,
data:
{
value:
"new-subgroup"
,
text:
new_subgroup_label
}
}
.menu-item
.icon-container
=
icon
(
"check"
,
class:
"list-item-checkmark"
)
.description
%strong
=
new_subgroup_label
%span
=
s_
(
"GroupsTree|Create a subgroup under this group."
)
=
render
"children"
,
children:
@children
-
if
params
[
:filter
].
blank?
&&
@children
.
empty?
=
render
"shared/groups/empty_state"
-
else
=
render
"children"
,
children:
@children
,
group:
@group
app/views/shared/groups/_dropdown.html.haml
View file @
de553961
.dropdown.inline.js-group-filter-dropdown-wrap
-
if
@sort
.
present?
-
default_sort_by
=
@sort
-
else
-
if
params
[
:sort
]
-
default_sort_by
=
params
[
:sort
]
-
else
-
default_sort_by
=
sort_value_recently_created
.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
%button
.dropdown-toggle
{
type:
'button'
,
'data-toggle'
=>
'dropdown'
}
%span
.dropdown-label
-
if
@sort
.
present?
=
sort_options_hash
[
@sort
]
-
else
=
sort_title_recently_created
=
sort_options_hash
[
default_sort_by
]
=
icon
(
'chevron-down'
)
%ul
.dropdown-menu.dropdown-menu-align-right
%li
=
link_to
filter_groups_path
(
sort:
sort_value_recently_created
)
do
=
sort_title_recently_created
=
link_to
filter_groups_path
(
sort:
sort_value_oldest_created
)
do
=
sort_title_oldest_created
=
link_to
filter_groups_path
(
sort:
sort_value_recently_updated
)
do
=
sort_title_recently_updated
=
link_to
filter_groups_path
(
sort:
sort_value_oldest_updated
)
do
=
sort_title_oldest_updated
%ul
.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li
.dropdown-header
=
_
(
"Sort by"
)
-
groups_sort_options_hash
.
each
do
|
value
,
title
|
%li
=
link_to
filter_groups_path
(
sort:
value
),
class:
"
#{
'is-active'
if
default_sort_by
==
value
}
"
do
=
title
app/views/shared/groups/_empty_state.html.haml
0 → 100644
View file @
de553961
.groups-empty-state
=
custom_icon
(
"icon_empty_groups"
)
.text-content
%h4
=
s_
(
"GroupsEmptyState|A group is a collection of several projects."
)
%p
=
s_
(
"GroupsEmptyState|If you organize your projects under a group, it works like a folder."
)
%p
=
s_
(
"GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
)
app/views/shared/groups/_group.html.haml
View file @
de553961
...
...
@@ -11,7 +11,7 @@
=
link_to
edit_group_path
(
group
),
class:
"btn"
do
=
icon
(
'cogs'
)
=
link_to
leave_group_group_members_path
(
group
),
data:
{
confirm:
leave_confirmation_message
(
group
)
},
method: :delete
,
class:
"btn"
,
title:
'Leave this group'
do
=
link_to
leave_group_group_members_path
(
group
),
data:
{
confirm:
leave_confirmation_message
(
group
)
},
method: :delete
,
class:
"btn"
,
title:
s_
(
"GroupsTree|Leave this group"
)
do
=
icon
(
'sign-out'
)
.stats
...
...
app/views/shared/groups/_list.html.haml
View file @
de553961
...
...
@@ -3,4 +3,4 @@
-
groups
.
each_with_index
do
|
group
,
i
|
=
render
"shared/groups/group"
,
group:
group
-
else
.nothing-here-block
No groups found
.nothing-here-block
=
s_
(
"GroupsEmptyState|No groups found"
)
app/views/shared/groups/_search_form.html.haml
View file @
de553961
=
form_tag
request
.
path
,
method: :get
,
class:
'group-filter-form'
,
id:
'group-filter-form'
do
|
f
|
=
search_field_tag
:filter
_groups
,
params
[
:filter_groups
],
placeholder:
'Filter by name...'
,
class:
'group-filter-form-field form-control input-short js-groups-list-filter'
,
spellcheck:
false
,
id:
'group-filter-form-field'
,
tabindex:
"2"
=
form_tag
request
.
path
,
method: :get
,
class:
'group-filter-form
append-right-10
'
,
id:
'group-filter-form'
do
|
f
|
=
search_field_tag
:filter
,
params
[
:filter
],
placeholder:
s_
(
'GroupsTree|Filter by name...'
)
,
class:
'group-filter-form-field form-control input-short js-groups-list-filter'
,
spellcheck:
false
,
id:
'group-filter-form-field'
,
tabindex:
"2"
app/views/shared/projects/_dropdown.html.haml
View file @
de553961
-
@sort
||=
sort_value_latest_activity
.dropdown
.dropdown
.js-project-filter-dropdown-wrap
-
toggle_text
=
projects_sort_options_hash
[
@sort
]
=
dropdown_toggle
(
toggle_text
,
{
toggle:
'dropdown'
},
{
id:
'sort-projects-dropdown'
})
%ul
.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
...
...
spec/features/dashboard/groups_list_spec.rb
View file @
de553961
...
...
@@ -16,6 +16,7 @@ feature 'Dashboard Groups page', :js do
sign_in
(
user
)
visit
dashboard_groups_path
wait_for_requests
expect
(
page
).
to
have_content
(
group
.
full_name
)
expect
(
page
).
to
have_content
(
nested_group
.
full_name
)
...
...
@@ -33,7 +34,7 @@ feature 'Dashboard Groups page', :js do
end
it
'filters groups'
do
fill_in
'filter
_groups
'
,
with:
group
.
name
fill_in
'filter'
,
with:
group
.
name
wait_for_requests
expect
(
page
).
to
have_content
(
group
.
full_name
)
...
...
@@ -42,10 +43,10 @@ feature 'Dashboard Groups page', :js do
end
it
'resets search when user cleans the input'
do
fill_in
'filter
_groups
'
,
with:
group
.
name
fill_in
'filter'
,
with:
group
.
name
wait_for_requests
fill_in
'filter
_groups
'
,
with:
''
fill_in
'filter'
,
with:
''
wait_for_requests
expect
(
page
).
to
have_content
(
group
.
full_name
)
...
...
spec/features/explore/groups_list_spec.rb
View file @
de553961
...
...
@@ -15,6 +15,7 @@ describe 'Explore Groups page', :js do
sign_in
(
user
)
visit
explore_groups_path
wait_for_requests
end
it
'shows groups user is member of'
do
...
...
@@ -24,7 +25,7 @@ describe 'Explore Groups page', :js do
end
it
'filters groups'
do
fill_in
'filter
_groups
'
,
with:
group
.
name
fill_in
'filter'
,
with:
group
.
name
wait_for_requests
expect
(
page
).
to
have_content
(
group
.
full_name
)
...
...
@@ -33,10 +34,10 @@ describe 'Explore Groups page', :js do
end
it
'resets search when user cleans the input'
do
fill_in
'filter
_groups
'
,
with:
group
.
name
fill_in
'filter'
,
with:
group
.
name
wait_for_requests
fill_in
'filter
_groups
'
,
with:
""
fill_in
'filter'
,
with:
""
wait_for_requests
expect
(
page
).
to
have_content
(
group
.
full_name
)
...
...
@@ -47,21 +48,21 @@ describe 'Explore Groups page', :js do
it
'shows non-archived projects count'
do
# Initially project is not archived
expect
(
find
(
'.js-groups-list-holder .content-list li:first-child .stats
span:first-child
'
)).
to
have_text
(
"1"
)
expect
(
find
(
'.js-groups-list-holder .content-list li:first-child .stats
.number-projects
'
)).
to
have_text
(
"1"
)
# Archive project
empty_project
.
archive!
visit
explore_groups_path
# Check project count
expect
(
find
(
'.js-groups-list-holder .content-list li:first-child .stats
span:first-child
'
)).
to
have_text
(
"0"
)
expect
(
find
(
'.js-groups-list-holder .content-list li:first-child .stats
.number-projects
'
)).
to
have_text
(
"0"
)
# Unarchive project
empty_project
.
unarchive!
visit
explore_groups_path
# Check project count
expect
(
find
(
'.js-groups-list-holder .content-list li:first-child .stats
span:first-child
'
)).
to
have_text
(
"1"
)
expect
(
find
(
'.js-groups-list-holder .content-list li:first-child .stats
.number-projects
'
)).
to
have_text
(
"1"
)
end
describe
'landing component'
do
...
...
spec/javascripts/groups/components/app_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
appComponent
from
'
~/groups/components/app.vue
'
;
import
groupFolderComponent
from
'
~/groups/components/group_folder.vue
'
;
import
groupItemComponent
from
'
~/groups/components/group_item.vue
'
;
import
eventHub
from
'
~/groups/event_hub
'
;
import
GroupsStore
from
'
~/groups/store/groups_store
'
;
import
GroupsService
from
'
~/groups/service/groups_service
'
;
import
{
mockEndpoint
,
mockGroups
,
mockSearchedGroups
,
mockRawPageInfo
,
mockParentGroupItem
,
mockRawChildren
,
mockChildren
,
mockPageInfo
,
}
from
'
../mock_data
'
;
const
createComponent
=
(
hideProjects
=
false
)
=>
{
const
Component
=
Vue
.
extend
(
appComponent
);
const
store
=
new
GroupsStore
(
false
);
const
service
=
new
GroupsService
(
mockEndpoint
);
return
new
Component
({
propsData
:
{
store
,
service
,
hideProjects
,
},
});
};
const
returnServicePromise
=
(
data
,
failed
)
=>
new
Promise
((
resolve
,
reject
)
=>
{
if
(
failed
)
{
reject
(
data
);
}
else
{
resolve
({
json
()
{
return
data
;
},
});
}
});
describe
(
'
AppComponent
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
Vue
.
component
(
'
group-folder
'
,
groupFolderComponent
);
Vue
.
component
(
'
group-item
'
,
groupItemComponent
);
vm
=
createComponent
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
describe
(
'
computed
'
,
()
=>
{
beforeEach
(()
=>
{
vm
.
$mount
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
groups
'
,
()
=>
{
it
(
'
should return list of groups from store
'
,
()
=>
{
spyOn
(
vm
.
store
,
'
getGroups
'
);
const
groups
=
vm
.
groups
;
expect
(
vm
.
store
.
getGroups
).
toHaveBeenCalled
();
expect
(
groups
).
not
.
toBeDefined
();
});
});
describe
(
'
pageInfo
'
,
()
=>
{
it
(
'
should return pagination info from store
'
,
()
=>
{
spyOn
(
vm
.
store
,
'
getPaginationInfo
'
);
const
pageInfo
=
vm
.
pageInfo
;
expect
(
vm
.
store
.
getPaginationInfo
).
toHaveBeenCalled
();
expect
(
pageInfo
).
not
.
toBeDefined
();
});
});
});
describe
(
'
methods
'
,
()
=>
{
beforeEach
(()
=>
{
vm
.
$mount
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
fetchGroups
'
,
()
=>
{
it
(
'
should call `getGroups` with all the params provided
'
,
(
done
)
=>
{
spyOn
(
vm
.
service
,
'
getGroups
'
).
and
.
returnValue
(
returnServicePromise
(
mockGroups
));
vm
.
fetchGroups
({
parentId
:
1
,
page
:
2
,
filterGroupsBy
:
'
git
'
,
sortBy
:
'
created_desc
'
,
});
setTimeout
(()
=>
{
expect
(
vm
.
service
.
getGroups
).
toHaveBeenCalledWith
(
1
,
2
,
'
git
'
,
'
created_desc
'
);
done
();
},
0
);
});
it
(
'
should set headers to store for building pagination info when called with `updatePagination`
'
,
(
done
)
=>
{
spyOn
(
vm
.
service
,
'
getGroups
'
).
and
.
returnValue
(
returnServicePromise
({
headers
:
mockRawPageInfo
}));
spyOn
(
vm
,
'
updatePagination
'
);
vm
.
fetchGroups
({
updatePagination
:
true
});
setTimeout
(()
=>
{
expect
(
vm
.
service
.
getGroups
).
toHaveBeenCalled
();
expect
(
vm
.
updatePagination
).
toHaveBeenCalled
();
done
();
},
0
);
});
it
(
'
should show flash error when request fails
'
,
(
done
)
=>
{
spyOn
(
vm
.
service
,
'
getGroups
'
).
and
.
returnValue
(
returnServicePromise
(
null
,
true
));
spyOn
(
$
,
'
scrollTo
'
);
spyOn
(
window
,
'
Flash
'
);
vm
.
fetchGroups
({});
setTimeout
(()
=>
{
expect
(
vm
.
isLoading
).
toBeFalsy
();
expect
(
$
.
scrollTo
).
toHaveBeenCalledWith
(
0
);
expect
(
window
.
Flash
).
toHaveBeenCalledWith
(
'
An error occurred. Please try again.
'
);
done
();
},
0
);
});
});
describe
(
'
fetchAllGroups
'
,
()
=>
{
it
(
'
should fetch default set of groups
'
,
(
done
)
=>
{
spyOn
(
vm
,
'
fetchGroups
'
).
and
.
returnValue
(
returnServicePromise
(
mockGroups
));
spyOn
(
vm
,
'
updatePagination
'
).
and
.
callThrough
();
spyOn
(
vm
,
'
updateGroups
'
).
and
.
callThrough
();
vm
.
fetchAllGroups
();
expect
(
vm
.
isLoading
).
toBeTruthy
();
expect
(
vm
.
fetchGroups
).
toHaveBeenCalled
();
setTimeout
(()
=>
{
expect
(
vm
.
isLoading
).
toBeFalsy
();
expect
(
vm
.
updateGroups
).
toHaveBeenCalled
();
done
();
},
0
);
});
it
(
'
should fetch matching set of groups when app is loaded with search query
'
,
(
done
)
=>
{
spyOn
(
vm
,
'
fetchGroups
'
).
and
.
returnValue
(
returnServicePromise
(
mockSearchedGroups
));
spyOn
(
vm
,
'
updateGroups
'
).
and
.
callThrough
();
vm
.
fetchAllGroups
();
expect
(
vm
.
fetchGroups
).
toHaveBeenCalledWith
({
page
:
null
,
filterGroupsBy
:
null
,
sortBy
:
null
,
updatePagination
:
true
,
});
setTimeout
(()
=>
{
expect
(
vm
.
updateGroups
).
toHaveBeenCalled
();
done
();
},
0
);
});
});
describe
(
'
fetchPage
'
,
()
=>
{
it
(
'
should fetch groups for provided page details and update window state
'
,
(
done
)
=>
{
spyOn
(
vm
,
'
fetchGroups
'
).
and
.
returnValue
(
returnServicePromise
(
mockGroups
));
spyOn
(
vm
,
'
updateGroups
'
).
and
.
callThrough
();
spyOn
(
gl
.
utils
,
'
mergeUrlParams
'
).
and
.
callThrough
();
spyOn
(
window
.
history
,
'
replaceState
'
);
spyOn
(
$
,
'
scrollTo
'
);
vm
.
fetchPage
(
2
,
null
,
null
);
expect
(
vm
.
isLoading
).
toBeTruthy
();
expect
(
vm
.
fetchGroups
).
toHaveBeenCalledWith
({
page
:
2
,
filterGroupsBy
:
null
,
sortBy
:
null
,
updatePagination
:
true
,
});
setTimeout
(()
=>
{
expect
(
vm
.
isLoading
).
toBeFalsy
();
expect
(
$
.
scrollTo
).
toHaveBeenCalledWith
(
0
);
expect
(
gl
.
utils
.
mergeUrlParams
).
toHaveBeenCalledWith
({
page
:
2
},
jasmine
.
any
(
String
));
expect
(
window
.
history
.
replaceState
).
toHaveBeenCalledWith
({
page
:
jasmine
.
any
(
String
),
},
jasmine
.
any
(
String
),
jasmine
.
any
(
String
));
expect
(
vm
.
updateGroups
).
toHaveBeenCalled
();
done
();
},
0
);
});
});
describe
(
'
toggleChildren
'
,
()
=>
{
let
groupItem
;
beforeEach
(()
=>
{
groupItem
=
Object
.
assign
({},
mockParentGroupItem
);
groupItem
.
isOpen
=
false
;
groupItem
.
isChildrenLoading
=
false
;
});
it
(
'
should fetch children of given group and expand it if group is collapsed and children are not loaded
'
,
(
done
)
=>
{
spyOn
(
vm
,
'
fetchGroups
'
).
and
.
returnValue
(
returnServicePromise
(
mockRawChildren
));
spyOn
(
vm
.
store
,
'
setGroupChildren
'
);
vm
.
toggleChildren
(
groupItem
);
expect
(
groupItem
.
isChildrenLoading
).
toBeTruthy
();
expect
(
vm
.
fetchGroups
).
toHaveBeenCalledWith
({
parentId
:
groupItem
.
id
,
});
setTimeout
(()
=>
{
expect
(
vm
.
store
.
setGroupChildren
).
toHaveBeenCalled
();
done
();
},
0
);
});
it
(
'
should skip network request while expanding group if children are already loaded
'
,
()
=>
{
spyOn
(
vm
,
'
fetchGroups
'
);
groupItem
.
children
=
mockRawChildren
;
vm
.
toggleChildren
(
groupItem
);
expect
(
vm
.
fetchGroups
).
not
.
toHaveBeenCalled
();
expect
(
groupItem
.
isOpen
).
toBeTruthy
();
});
it
(
'
should collapse group if it is already expanded
'
,
()
=>
{
spyOn
(
vm
,
'
fetchGroups
'
);
groupItem
.
isOpen
=
true
;
vm
.
toggleChildren
(
groupItem
);
expect
(
vm
.
fetchGroups
).
not
.
toHaveBeenCalled
();
expect
(
groupItem
.
isOpen
).
toBeFalsy
();
});
it
(
'
should set `isChildrenLoading` back to `false` if load request fails
'
,
(
done
)
=>
{
spyOn
(
vm
,
'
fetchGroups
'
).
and
.
returnValue
(
returnServicePromise
({},
true
));
vm
.
toggleChildren
(
groupItem
);
expect
(
groupItem
.
isChildrenLoading
).
toBeTruthy
();
setTimeout
(()
=>
{
expect
(
groupItem
.
isChildrenLoading
).
toBeFalsy
();
done
();
},
0
);
});
});
describe
(
'
leaveGroup
'
,
()
=>
{
let
groupItem
;
let
childGroupItem
;
beforeEach
(()
=>
{
groupItem
=
Object
.
assign
({},
mockParentGroupItem
);
groupItem
.
children
=
mockChildren
;
childGroupItem
=
groupItem
.
children
[
0
];
groupItem
.
isChildrenLoading
=
false
;
});
it
(
'
should leave group and remove group item from tree
'
,
(
done
)
=>
{
const
notice
=
`You left the "
${
childGroupItem
.
fullName
}
" group.`
;
spyOn
(
vm
.
service
,
'
leaveGroup
'
).
and
.
returnValue
(
returnServicePromise
({
notice
}));
spyOn
(
vm
.
store
,
'
removeGroup
'
).
and
.
callThrough
();
spyOn
(
window
,
'
Flash
'
);
spyOn
(
$
,
'
scrollTo
'
);
vm
.
leaveGroup
(
childGroupItem
,
groupItem
);
expect
(
childGroupItem
.
isBeingRemoved
).
toBeTruthy
();
expect
(
vm
.
service
.
leaveGroup
).
toHaveBeenCalledWith
(
childGroupItem
.
leavePath
);
setTimeout
(()
=>
{
expect
(
$
.
scrollTo
).
toHaveBeenCalledWith
(
0
);
expect
(
vm
.
store
.
removeGroup
).
toHaveBeenCalledWith
(
childGroupItem
,
groupItem
);
expect
(
window
.
Flash
).
toHaveBeenCalledWith
(
notice
,
'
notice
'
);
done
();
},
0
);
});
it
(
'
should show error flash message if request failed to leave group
'
,
(
done
)
=>
{
const
message
=
'
An error occurred. Please try again.
'
;
spyOn
(
vm
.
service
,
'
leaveGroup
'
).
and
.
returnValue
(
returnServicePromise
({
status
:
500
},
true
));
spyOn
(
vm
.
store
,
'
removeGroup
'
).
and
.
callThrough
();
spyOn
(
window
,
'
Flash
'
);
vm
.
leaveGroup
(
childGroupItem
,
groupItem
);
expect
(
childGroupItem
.
isBeingRemoved
).
toBeTruthy
();
expect
(
vm
.
service
.
leaveGroup
).
toHaveBeenCalledWith
(
childGroupItem
.
leavePath
);
setTimeout
(()
=>
{
expect
(
vm
.
store
.
removeGroup
).
not
.
toHaveBeenCalled
();
expect
(
window
.
Flash
).
toHaveBeenCalledWith
(
message
);
expect
(
childGroupItem
.
isBeingRemoved
).
toBeFalsy
();
done
();
},
0
);
});
it
(
'
should show appropriate error flash message if request forbids to leave group
'
,
(
done
)
=>
{
const
message
=
'
Failed to leave the group. Please make sure you are not the only owner.
'
;
spyOn
(
vm
.
service
,
'
leaveGroup
'
).
and
.
returnValue
(
returnServicePromise
({
status
:
403
},
true
));
spyOn
(
vm
.
store
,
'
removeGroup
'
).
and
.
callThrough
();
spyOn
(
window
,
'
Flash
'
);
vm
.
leaveGroup
(
childGroupItem
,
groupItem
);
expect
(
childGroupItem
.
isBeingRemoved
).
toBeTruthy
();
expect
(
vm
.
service
.
leaveGroup
).
toHaveBeenCalledWith
(
childGroupItem
.
leavePath
);
setTimeout
(()
=>
{
expect
(
vm
.
store
.
removeGroup
).
not
.
toHaveBeenCalled
();
expect
(
window
.
Flash
).
toHaveBeenCalledWith
(
message
);
expect
(
childGroupItem
.
isBeingRemoved
).
toBeFalsy
();
done
();
},
0
);
});
});
describe
(
'
updatePagination
'
,
()
=>
{
it
(
'
should set pagination info to store from provided headers
'
,
()
=>
{
spyOn
(
vm
.
store
,
'
setPaginationInfo
'
);
vm
.
updatePagination
(
mockRawPageInfo
);
expect
(
vm
.
store
.
setPaginationInfo
).
toHaveBeenCalledWith
(
mockRawPageInfo
);
});
});
describe
(
'
updateGroups
'
,
()
=>
{
it
(
'
should call setGroups on store if method was called directly
'
,
()
=>
{
spyOn
(
vm
.
store
,
'
setGroups
'
);
vm
.
updateGroups
(
mockGroups
);
expect
(
vm
.
store
.
setGroups
).
toHaveBeenCalledWith
(
mockGroups
);
});
it
(
'
should call setSearchedGroups on store if method was called with fromSearch param
'
,
()
=>
{
spyOn
(
vm
.
store
,
'
setSearchedGroups
'
);
vm
.
updateGroups
(
mockGroups
,
true
);
expect
(
vm
.
store
.
setSearchedGroups
).
toHaveBeenCalledWith
(
mockGroups
);
});
it
(
'
should set `isSearchEmpty` prop based on groups count
'
,
()
=>
{
vm
.
updateGroups
(
mockGroups
);
expect
(
vm
.
isSearchEmpty
).
toBeFalsy
();
vm
.
updateGroups
([]);
expect
(
vm
.
isSearchEmpty
).
toBeTruthy
();
});
});
});
describe
(
'
created
'
,
()
=>
{
it
(
'
should bind event listeners on eventHub
'
,
(
done
)
=>
{
spyOn
(
eventHub
,
'
$on
'
);
const
newVm
=
createComponent
();
newVm
.
$mount
();
Vue
.
nextTick
(()
=>
{
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'
fetchPage
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'
toggleChildren
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'
leaveGroup
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'
updatePagination
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'
updateGroups
'
,
jasmine
.
any
(
Function
));
newVm
.
$destroy
();
done
();
});
});
it
(
'
should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`
'
,
(
done
)
=>
{
const
newVm
=
createComponent
();
newVm
.
$mount
();
Vue
.
nextTick
(()
=>
{
expect
(
newVm
.
searchEmptyMessage
).
toBe
(
'
Sorry, no groups or projects matched your search
'
);
newVm
.
$destroy
();
done
();
});
});
it
(
'
should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`
'
,
(
done
)
=>
{
const
newVm
=
createComponent
(
true
);
newVm
.
$mount
();
Vue
.
nextTick
(()
=>
{
expect
(
newVm
.
searchEmptyMessage
).
toBe
(
'
Sorry, no groups matched your search
'
);
newVm
.
$destroy
();
done
();
});
});
});
describe
(
'
beforeDestroy
'
,
()
=>
{
it
(
'
should unbind event listeners on eventHub
'
,
(
done
)
=>
{
spyOn
(
eventHub
,
'
$off
'
);
const
newVm
=
createComponent
();
newVm
.
$mount
();
newVm
.
$destroy
();
Vue
.
nextTick
(()
=>
{
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'
fetchPage
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'
toggleChildren
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'
leaveGroup
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'
updatePagination
'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'
updateGroups
'
,
jasmine
.
any
(
Function
));
done
();
});
});
});
describe
(
'
template
'
,
()
=>
{
beforeEach
(()
=>
{
vm
.
$mount
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'
should render loading icon
'
,
(
done
)
=>
{
vm
.
isLoading
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.loading-animation
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
i.fa
'
).
getAttribute
(
'
aria-label
'
)).
toBe
(
'
Loading groups
'
);
done
();
});
});
it
(
'
should render groups tree
'
,
(
done
)
=>
{
vm
.
groups
=
[
mockParentGroupItem
];
vm
.
isLoading
=
false
;
vm
.
pageInfo
=
mockPageInfo
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.groups-list-tree-container
'
)).
toBeDefined
();
done
();
});
});
});
});
spec/javascripts/groups/components/group_folder_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
groupFolderComponent
from
'
~/groups/components/group_folder.vue
'
;
import
groupItemComponent
from
'
~/groups/components/group_item.vue
'
;
import
{
mockGroups
,
mockParentGroupItem
}
from
'
../mock_data
'
;
const
createComponent
=
(
groups
=
mockGroups
,
parentGroup
=
mockParentGroupItem
)
=>
{
const
Component
=
Vue
.
extend
(
groupFolderComponent
);
return
new
Component
({
propsData
:
{
groups
,
parentGroup
,
},
});
};
describe
(
'
GroupFolderComponent
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
Vue
.
component
(
'
group-item
'
,
groupItemComponent
);
vm
=
createComponent
();
vm
.
$mount
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
hasMoreChildren
'
,
()
=>
{
it
(
'
should return false when childrenCount of group is less than MAX_CHILDREN_COUNT
'
,
()
=>
{
expect
(
vm
.
hasMoreChildren
).
toBeFalsy
();
});
});
describe
(
'
moreChildrenStats
'
,
()
=>
{
it
(
'
should return message with count of excess children over MAX_CHILDREN_COUNT limit
'
,
()
=>
{
expect
(
vm
.
moreChildrenStats
).
toBe
(
'
3 more items
'
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render component template correctly
'
,
()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
group-list-tree
'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
li.group-row
'
).
length
).
toBe
(
7
);
});
it
(
'
should render more children link when groups list has children over MAX_CHILDREN_COUNT limit
'
,
()
=>
{
const
parentGroup
=
Object
.
assign
({},
mockParentGroupItem
);
parentGroup
.
childrenCount
=
21
;
const
newVm
=
createComponent
(
mockGroups
,
parentGroup
);
newVm
.
$mount
();
expect
(
newVm
.
$el
.
querySelector
(
'
li.group-row a.has-more-items
'
)).
toBeDefined
();
newVm
.
$destroy
();
});
});
});
spec/javascripts/groups/components/group_item_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
groupItemComponent
from
'
~/groups/components/group_item.vue
'
;
import
groupFolderComponent
from
'
~/groups/components/group_folder.vue
'
;
import
eventHub
from
'
~/groups/event_hub
'
;
import
{
mockParentGroupItem
,
mockChildren
}
from
'
../mock_data
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
createComponent
=
(
group
=
mockParentGroupItem
,
parentGroup
=
mockChildren
[
0
])
=>
{
const
Component
=
Vue
.
extend
(
groupItemComponent
);
return
mountComponent
(
Component
,
{
group
,
parentGroup
,
});
};
describe
(
'
GroupItemComponent
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
Vue
.
component
(
'
group-folder
'
,
groupFolderComponent
);
vm
=
createComponent
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
groupDomId
'
,
()
=>
{
it
(
'
should return ID string suffixed with group ID
'
,
()
=>
{
expect
(
vm
.
groupDomId
).
toBe
(
'
group-55
'
);
});
});
describe
(
'
rowClass
'
,
()
=>
{
it
(
'
should return map of classes based on group details
'
,
()
=>
{
const
classes
=
[
'
is-open
'
,
'
has-children
'
,
'
has-description
'
,
'
being-removed
'
];
const
rowClass
=
vm
.
rowClass
;
expect
(
Object
.
keys
(
rowClass
).
length
).
toBe
(
classes
.
length
);
Object
.
keys
(
rowClass
).
forEach
((
className
)
=>
{
expect
(
classes
.
indexOf
(
className
)
>
-
1
).
toBeTruthy
();
});
});
});
describe
(
'
hasChildren
'
,
()
=>
{
it
(
'
should return boolean value representing if group has any children present
'
,
()
=>
{
let
newVm
;
const
group
=
Object
.
assign
({},
mockParentGroupItem
);
group
.
childrenCount
=
5
;
newVm
=
createComponent
(
group
);
expect
(
newVm
.
hasChildren
).
toBeTruthy
();
newVm
.
$destroy
();
group
.
childrenCount
=
0
;
newVm
=
createComponent
(
group
);
expect
(
newVm
.
hasChildren
).
toBeFalsy
();
newVm
.
$destroy
();
});
});
describe
(
'
hasAvatar
'
,
()
=>
{
it
(
'
should return boolean value representing if group has any avatar present
'
,
()
=>
{
let
newVm
;
const
group
=
Object
.
assign
({},
mockParentGroupItem
);
group
.
avatarUrl
=
null
;
newVm
=
createComponent
(
group
);
expect
(
newVm
.
hasAvatar
).
toBeFalsy
();
newVm
.
$destroy
();
group
.
avatarUrl
=
'
/uploads/group_avatar.png
'
;
newVm
=
createComponent
(
group
);
expect
(
newVm
.
hasAvatar
).
toBeTruthy
();
newVm
.
$destroy
();
});
});
describe
(
'
isGroup
'
,
()
=>
{
it
(
'
should return boolean value representing if group item is of type `group` or not
'
,
()
=>
{
let
newVm
;
const
group
=
Object
.
assign
({},
mockParentGroupItem
);
group
.
type
=
'
group
'
;
newVm
=
createComponent
(
group
);
expect
(
newVm
.
isGroup
).
toBeTruthy
();
newVm
.
$destroy
();
group
.
type
=
'
project
'
;
newVm
=
createComponent
(
group
);
expect
(
newVm
.
isGroup
).
toBeFalsy
();
newVm
.
$destroy
();
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
onClickRowGroup
'
,
()
=>
{
let
event
;
beforeEach
(()
=>
{
const
classList
=
{
contains
()
{
return
false
;
},
};
event
=
{
target
:
{
classList
,
parentElement
:
{
classList
,
},
},
};
});
it
(
'
should emit `toggleChildren` event when expand is clicked on a group and it has children present
'
,
()
=>
{
spyOn
(
eventHub
,
'
$emit
'
);
vm
.
onClickRowGroup
(
event
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
toggleChildren
'
,
vm
.
group
);
});
it
(
'
should navigate page to group homepage if group does not have any children present
'
,
(
done
)
=>
{
const
group
=
Object
.
assign
({},
mockParentGroupItem
);
group
.
childrenCount
=
0
;
const
newVm
=
createComponent
(
group
);
spyOn
(
gl
.
utils
,
'
visitUrl
'
).
and
.
stub
();
spyOn
(
eventHub
,
'
$emit
'
);
newVm
.
onClickRowGroup
(
event
);
setTimeout
(()
=>
{
expect
(
eventHub
.
$emit
).
not
.
toHaveBeenCalled
();
expect
(
gl
.
utils
.
visitUrl
).
toHaveBeenCalledWith
(
newVm
.
group
.
relativePath
);
done
();
},
0
);
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render component template correctly
'
,
()
=>
{
expect
(
vm
.
$el
.
getAttribute
(
'
id
'
)).
toBe
(
'
group-55
'
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
group-row
'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
querySelector
(
'
.group-row-contents
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.group-row-contents .controls
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.group-row-contents .stats
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.folder-toggle-wrap
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.folder-toggle-wrap .folder-caret
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.folder-toggle-wrap .item-type-icon
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.avatar-container
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.avatar-container a.no-expand
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.avatar-container .avatar
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.title
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.title a.no-expand
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.access-type
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.description
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.group-list-tree
'
)).
toBeDefined
();
});
});
});
spec/javascripts/groups/components/groups_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
groupsComponent
from
'
~/groups/components/groups.vue
'
;
import
groupFolderComponent
from
'
~/groups/components/group_folder.vue
'
;
import
groupItemComponent
from
'
~/groups/components/group_item.vue
'
;
import
eventHub
from
'
~/groups/event_hub
'
;
import
{
mockGroups
,
mockPageInfo
}
from
'
../mock_data
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
createComponent
=
(
searchEmpty
=
false
)
=>
{
const
Component
=
Vue
.
extend
(
groupsComponent
);
return
mountComponent
(
Component
,
{
groups
:
mockGroups
,
pageInfo
:
mockPageInfo
,
searchEmptyMessage
:
'
No matching results
'
,
searchEmpty
,
});
};
describe
(
'
GroupsComponent
'
,
()
=>
{
let
vm
;
beforeEach
((
done
)
=>
{
Vue
.
component
(
'
group-folder
'
,
groupFolderComponent
);
Vue
.
component
(
'
group-item
'
,
groupItemComponent
);
vm
=
createComponent
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
change
'
,
()
=>
{
it
(
'
should emit `fetchPage` event when page is changed via pagination
'
,
()
=>
{
spyOn
(
eventHub
,
'
$emit
'
).
and
.
stub
();
vm
.
change
(
2
);
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
fetchPage
'
,
2
,
jasmine
.
any
(
Object
),
jasmine
.
any
(
Object
));
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render component template correctly
'
,
(
done
)
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.groups-list-tree-container
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.group-list-tree
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.gl-pagination
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
.has-no-search-results
'
).
length
===
0
).
toBeTruthy
();
done
();
});
});
it
(
'
should render empty search message when `searchEmpty` is `true`
'
,
(
done
)
=>
{
vm
.
searchEmpty
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.has-no-search-results
'
)).
toBeDefined
();
done
();
});
});
});
});
spec/javascripts/groups/components/item_actions_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
itemActionsComponent
from
'
~/groups/components/item_actions.vue
'
;
import
eventHub
from
'
~/groups/event_hub
'
;
import
{
mockParentGroupItem
,
mockChildren
}
from
'
../mock_data
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
createComponent
=
(
group
=
mockParentGroupItem
,
parentGroup
=
mockChildren
[
0
])
=>
{
const
Component
=
Vue
.
extend
(
itemActionsComponent
);
return
mountComponent
(
Component
,
{
group
,
parentGroup
,
});
};
describe
(
'
ItemActionsComponent
'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'
computed
'
,
()
=>
{
describe
(
'
leaveConfirmationMessage
'
,
()
=>
{
it
(
'
should return appropriate string for leave group confirmation
'
,
()
=>
{
expect
(
vm
.
leaveConfirmationMessage
).
toBe
(
'
Are you sure you want to leave the "platform / hardware" group?
'
);
});
});
});
describe
(
'
methods
'
,
()
=>
{
describe
(
'
onLeaveGroup
'
,
()
=>
{
it
(
'
should change `dialogStatus` prop to `true` which shows confirmation dialog
'
,
()
=>
{
expect
(
vm
.
dialogStatus
).
toBeFalsy
();
vm
.
onLeaveGroup
();
expect
(
vm
.
dialogStatus
).
toBeTruthy
();
});
});
describe
(
'
leaveGroup
'
,
()
=>
{
it
(
'
should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`
'
,
()
=>
{
spyOn
(
eventHub
,
'
$emit
'
);
vm
.
dialogStatus
=
true
;
vm
.
leaveGroup
(
true
);
expect
(
vm
.
dialogStatus
).
toBeFalsy
();
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'
leaveGroup
'
,
vm
.
group
,
vm
.
parentGroup
);
});
it
(
'
should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`
'
,
()
=>
{
spyOn
(
eventHub
,
'
$emit
'
);
vm
.
dialogStatus
=
true
;
vm
.
leaveGroup
(
false
);
expect
(
vm
.
dialogStatus
).
toBeFalsy
();
expect
(
eventHub
.
$emit
).
not
.
toHaveBeenCalled
();
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render component template correctly
'
,
()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'
controls
'
)).
toBeTruthy
();
});
it
(
'
should render Edit Group button with correct attribute values
'
,
()
=>
{
const
group
=
Object
.
assign
({},
mockParentGroupItem
);
group
.
canEdit
=
true
;
const
newVm
=
createComponent
(
group
);
const
editBtn
=
newVm
.
$el
.
querySelector
(
'
a.edit-group
'
);
expect
(
editBtn
).
toBeDefined
();
expect
(
editBtn
.
classList
.
contains
(
'
no-expand
'
)).
toBeTruthy
();
expect
(
editBtn
.
getAttribute
(
'
href
'
)).
toBe
(
group
.
editPath
);
expect
(
editBtn
.
getAttribute
(
'
aria-label
'
)).
toBe
(
'
Edit group
'
);
expect
(
editBtn
.
dataset
.
originalTitle
).
toBe
(
'
Edit group
'
);
expect
(
editBtn
.
querySelector
(
'
i.fa.fa-cogs
'
)).
toBeDefined
();
newVm
.
$destroy
();
});
it
(
'
should render Leave Group button with correct attribute values
'
,
()
=>
{
const
group
=
Object
.
assign
({},
mockParentGroupItem
);
group
.
canLeave
=
true
;
const
newVm
=
createComponent
(
group
);
const
leaveBtn
=
newVm
.
$el
.
querySelector
(
'
a.leave-group
'
);
expect
(
leaveBtn
).
toBeDefined
();
expect
(
leaveBtn
.
classList
.
contains
(
'
no-expand
'
)).
toBeTruthy
();
expect
(
leaveBtn
.
getAttribute
(
'
href
'
)).
toBe
(
group
.
leavePath
);
expect
(
leaveBtn
.
getAttribute
(
'
aria-label
'
)).
toBe
(
'
Leave this group
'
);
expect
(
leaveBtn
.
dataset
.
originalTitle
).
toBe
(
'
Leave this group
'
);
expect
(
leaveBtn
.
querySelector
(
'
i.fa.fa-sign-out
'
)).
toBeDefined
();
newVm
.
$destroy
();
});
it
(
'
should show modal dialog when `dialogStatus` is set to `true`
'
,
()
=>
{
vm
.
dialogStatus
=
true
;
const
modalDialogEl
=
vm
.
$el
.
querySelector
(
'
.modal.popup-dialog
'
);
expect
(
modalDialogEl
).
toBeDefined
();
expect
(
modalDialogEl
.
querySelector
(
'
.modal-title
'
).
innerText
.
trim
()).
toBe
(
'
Are you sure?
'
);
expect
(
modalDialogEl
.
querySelector
(
'
.btn.btn-warning
'
).
innerText
.
trim
()).
toBe
(
'
Leave
'
);
});
});
});
spec/javascripts/groups/components/item_caret_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
itemCaretComponent
from
'
~/groups/components/item_caret.vue
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
createComponent
=
(
isGroupOpen
=
false
)
=>
{
const
Component
=
Vue
.
extend
(
itemCaretComponent
);
return
mountComponent
(
Component
,
{
isGroupOpen
,
});
};
describe
(
'
ItemCaretComponent
'
,
()
=>
{
describe
(
'
template
'
,
()
=>
{
it
(
'
should render component template correctly
'
,
()
=>
{
const
vm
=
createComponent
();
vm
.
$mount
();
expect
(
vm
.
$el
.
classList
.
contains
(
'
folder-caret
'
)).
toBeTruthy
();
vm
.
$destroy
();
});
it
(
'
should render caret down icon if `isGroupOpen` prop is `true`
'
,
()
=>
{
const
vm
=
createComponent
(
true
);
vm
.
$mount
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
i.fa.fa-caret-down
'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
i.fa.fa-caret-right
'
).
length
).
toBe
(
0
);
vm
.
$destroy
();
});
it
(
'
should render caret right icon if `isGroupOpen` prop is `false`
'
,
()
=>
{
const
vm
=
createComponent
();
vm
.
$mount
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
i.fa.fa-caret-down
'
).
length
).
toBe
(
0
);
expect
(
vm
.
$el
.
querySelectorAll
(
'
i.fa.fa-caret-right
'
).
length
).
toBe
(
1
);
vm
.
$destroy
();
});
});
});
spec/javascripts/groups/components/item_stats_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
itemStatsComponent
from
'
~/groups/components/item_stats.vue
'
;
import
{
mockParentGroupItem
,
ITEM_TYPE
,
VISIBILITY_TYPE_ICON
,
GROUP_VISIBILITY_TYPE
,
PROJECT_VISIBILITY_TYPE
,
}
from
'
../mock_data
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
createComponent
=
(
item
=
mockParentGroupItem
)
=>
{
const
Component
=
Vue
.
extend
(
itemStatsComponent
);
return
mountComponent
(
Component
,
{
item
,
});
};
describe
(
'
ItemStatsComponent
'
,
()
=>
{
describe
(
'
computed
'
,
()
=>
{
describe
(
'
visibilityIcon
'
,
()
=>
{
it
(
'
should return icon class based on `item.visibility` value
'
,
()
=>
{
Object
.
keys
(
VISIBILITY_TYPE_ICON
).
forEach
((
visibility
)
=>
{
const
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
visibility
});
const
vm
=
createComponent
(
item
);
vm
.
$mount
();
expect
(
vm
.
visibilityIcon
).
toBe
(
VISIBILITY_TYPE_ICON
[
visibility
]);
vm
.
$destroy
();
});
});
});
describe
(
'
visibilityTooltip
'
,
()
=>
{
it
(
'
should return tooltip string for Group based on `item.visibility` value
'
,
()
=>
{
Object
.
keys
(
GROUP_VISIBILITY_TYPE
).
forEach
((
visibility
)
=>
{
const
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
visibility
,
type
:
ITEM_TYPE
.
GROUP
,
});
const
vm
=
createComponent
(
item
);
vm
.
$mount
();
expect
(
vm
.
visibilityTooltip
).
toBe
(
GROUP_VISIBILITY_TYPE
[
visibility
]);
vm
.
$destroy
();
});
});
it
(
'
should return tooltip string for Project based on `item.visibility` value
'
,
()
=>
{
Object
.
keys
(
PROJECT_VISIBILITY_TYPE
).
forEach
((
visibility
)
=>
{
const
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
visibility
,
type
:
ITEM_TYPE
.
PROJECT
,
});
const
vm
=
createComponent
(
item
);
vm
.
$mount
();
expect
(
vm
.
visibilityTooltip
).
toBe
(
PROJECT_VISIBILITY_TYPE
[
visibility
]);
vm
.
$destroy
();
});
});
});
describe
(
'
isProject
'
,
()
=>
{
it
(
'
should return boolean value representing whether `item.type` is Project or not
'
,
()
=>
{
let
item
;
let
vm
;
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
type
:
ITEM_TYPE
.
PROJECT
});
vm
=
createComponent
(
item
);
vm
.
$mount
();
expect
(
vm
.
isProject
).
toBeTruthy
();
vm
.
$destroy
();
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
type
:
ITEM_TYPE
.
GROUP
});
vm
=
createComponent
(
item
);
vm
.
$mount
();
expect
(
vm
.
isProject
).
toBeFalsy
();
vm
.
$destroy
();
});
});
describe
(
'
isGroup
'
,
()
=>
{
it
(
'
should return boolean value representing whether `item.type` is Group or not
'
,
()
=>
{
let
item
;
let
vm
;
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
type
:
ITEM_TYPE
.
GROUP
});
vm
=
createComponent
(
item
);
vm
.
$mount
();
expect
(
vm
.
isGroup
).
toBeTruthy
();
vm
.
$destroy
();
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
type
:
ITEM_TYPE
.
PROJECT
});
vm
=
createComponent
(
item
);
vm
.
$mount
();
expect
(
vm
.
isGroup
).
toBeFalsy
();
vm
.
$destroy
();
});
});
});
describe
(
'
template
'
,
()
=>
{
it
(
'
should render component template correctly
'
,
()
=>
{
const
vm
=
createComponent
();
vm
.
$mount
();
const
visibilityIconEl
=
vm
.
$el
.
querySelector
(
'
.item-visibility
'
);
expect
(
vm
.
$el
.
classList
.
contains
(
'
.stats
'
)).
toBeDefined
();
expect
(
visibilityIconEl
).
toBeDefined
();
expect
(
visibilityIconEl
.
dataset
.
originalTitle
).
toBe
(
vm
.
visibilityTooltip
);
expect
(
visibilityIconEl
.
querySelector
(
'
i.fa
'
)).
toBeDefined
();
vm
.
$destroy
();
});
it
(
'
should render stat icons if `item.type` is Group
'
,
()
=>
{
const
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
type
:
ITEM_TYPE
.
GROUP
});
const
vm
=
createComponent
(
item
);
vm
.
$mount
();
const
subgroupIconEl
=
vm
.
$el
.
querySelector
(
'
span.number-subgroups
'
);
expect
(
subgroupIconEl
).
toBeDefined
();
expect
(
subgroupIconEl
.
dataset
.
originalTitle
).
toBe
(
'
Subgroups
'
);
expect
(
subgroupIconEl
.
querySelector
(
'
i.fa.fa-folder
'
)).
toBeDefined
();
expect
(
subgroupIconEl
.
innerText
.
trim
()).
toBe
(
`
${
vm
.
item
.
subgroupCount
}
`
);
const
projectsIconEl
=
vm
.
$el
.
querySelector
(
'
span.number-projects
'
);
expect
(
projectsIconEl
).
toBeDefined
();
expect
(
projectsIconEl
.
dataset
.
originalTitle
).
toBe
(
'
Projects
'
);
expect
(
projectsIconEl
.
querySelector
(
'
i.fa.fa-bookmark
'
)).
toBeDefined
();
expect
(
projectsIconEl
.
innerText
.
trim
()).
toBe
(
`
${
vm
.
item
.
projectCount
}
`
);
const
membersIconEl
=
vm
.
$el
.
querySelector
(
'
span.number-users
'
);
expect
(
membersIconEl
).
toBeDefined
();
expect
(
membersIconEl
.
dataset
.
originalTitle
).
toBe
(
'
Members
'
);
expect
(
membersIconEl
.
querySelector
(
'
i.fa.fa-users
'
)).
toBeDefined
();
expect
(
membersIconEl
.
innerText
.
trim
()).
toBe
(
`
${
vm
.
item
.
memberCount
}
`
);
vm
.
$destroy
();
});
it
(
'
should render stat icons if `item.type` is Project
'
,
()
=>
{
const
item
=
Object
.
assign
({},
mockParentGroupItem
,
{
type
:
ITEM_TYPE
.
PROJECT
,
starCount
:
4
,
});
const
vm
=
createComponent
(
item
);
vm
.
$mount
();
const
projectStarIconEl
=
vm
.
$el
.
querySelector
(
'
.project-stars
'
);
expect
(
projectStarIconEl
).
toBeDefined
();
expect
(
projectStarIconEl
.
querySelector
(
'
i.fa.fa-star
'
)).
toBeDefined
();
expect
(
projectStarIconEl
.
innerText
.
trim
()).
toBe
(
`
${
vm
.
item
.
starCount
}
`
);
vm
.
$destroy
();
});
});
});
spec/javascripts/groups/components/item_type_icon_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
itemTypeIconComponent
from
'
~/groups/components/item_type_icon.vue
'
;
import
{
ITEM_TYPE
}
from
'
../mock_data
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
createComponent
=
(
itemType
=
ITEM_TYPE
.
GROUP
,
isGroupOpen
=
false
)
=>
{
const
Component
=
Vue
.
extend
(
itemTypeIconComponent
);
return
mountComponent
(
Component
,
{
itemType
,
isGroupOpen
,
});
};
describe
(
'
ItemTypeIconComponent
'
,
()
=>
{
describe
(
'
template
'
,
()
=>
{
it
(
'
should render component template correctly
'
,
()
=>
{
const
vm
=
createComponent
();
vm
.
$mount
();
expect
(
vm
.
$el
.
classList
.
contains
(
'
item-type-icon
'
)).
toBeTruthy
();
vm
.
$destroy
();
});
it
(
'
should render folder open or close icon based `isGroupOpen` prop value
'
,
()
=>
{
let
vm
;
vm
=
createComponent
(
ITEM_TYPE
.
GROUP
,
true
);
vm
.
$mount
();
expect
(
vm
.
$el
.
querySelector
(
'
i.fa.fa-folder-open
'
)).
toBeDefined
();
vm
.
$destroy
();
vm
=
createComponent
(
ITEM_TYPE
.
GROUP
);
vm
.
$mount
();
expect
(
vm
.
$el
.
querySelector
(
'
i.fa.fa-folder
'
)).
toBeDefined
();
vm
.
$destroy
();
});
it
(
'
should render bookmark icon based on `isProject` prop value
'
,
()
=>
{
let
vm
;
vm
=
createComponent
(
ITEM_TYPE
.
PROJECT
);
vm
.
$mount
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
i.fa.fa-bookmark
'
).
length
).
toBe
(
1
);
vm
.
$destroy
();
vm
=
createComponent
(
ITEM_TYPE
.
GROUP
);
vm
.
$mount
();
expect
(
vm
.
$el
.
querySelectorAll
(
'
i.fa.fa-bookmark
'
).
length
).
toBe
(
0
);
vm
.
$destroy
();
});
});
});
spec/javascripts/groups/group_item_spec.js
deleted
100644 → 0
View file @
67815272
import
Vue
from
'
vue
'
;
import
groupItemComponent
from
'
~/groups/components/group_item.vue
'
;
import
GroupsStore
from
'
~/groups/stores/groups_store
'
;
import
{
group1
}
from
'
./mock_data
'
;
describe
(
'
Groups Component
'
,
()
=>
{
let
GroupItemComponent
;
let
component
;
let
store
;
let
group
;
describe
(
'
group with default data
'
,
()
=>
{
beforeEach
((
done
)
=>
{
GroupItemComponent
=
Vue
.
extend
(
groupItemComponent
);
store
=
new
GroupsStore
();
group
=
store
.
decorateGroup
(
group1
);
component
=
new
GroupItemComponent
({
propsData
:
{
group
,
},
}).
$mount
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
afterEach
(()
=>
{
component
.
$destroy
();
});
it
(
'
should render the group item correctly
'
,
()
=>
{
expect
(
component
.
$el
.
classList
.
contains
(
'
group-row
'
)).
toBe
(
true
);
expect
(
component
.
$el
.
classList
.
contains
(
'
.no-description
'
)).
toBe
(
false
);
expect
(
component
.
$el
.
querySelector
(
'
.number-projects
'
).
textContent
).
toContain
(
group
.
numberProjects
);
expect
(
component
.
$el
.
querySelector
(
'
.number-users
'
).
textContent
).
toContain
(
group
.
numberUsers
);
expect
(
component
.
$el
.
querySelector
(
'
.group-visibility
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.avatar-container
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.title
'
).
textContent
).
toContain
(
group
.
name
);
expect
(
component
.
$el
.
querySelector
(
'
.access-type
'
).
textContent
).
toContain
(
group
.
permissions
.
humanGroupAccess
);
expect
(
component
.
$el
.
querySelector
(
'
.description
'
).
textContent
).
toContain
(
group
.
description
);
expect
(
component
.
$el
.
querySelector
(
'
.edit-group
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
.leave-group
'
)).
toBeDefined
();
});
});
describe
(
'
group without description
'
,
()
=>
{
beforeEach
((
done
)
=>
{
GroupItemComponent
=
Vue
.
extend
(
groupItemComponent
);
store
=
new
GroupsStore
();
group1
.
description
=
''
;
group
=
store
.
decorateGroup
(
group1
);
component
=
new
GroupItemComponent
({
propsData
:
{
group
,
},
}).
$mount
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
afterEach
(()
=>
{
component
.
$destroy
();
});
it
(
'
should render group item correctly
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.description
'
).
textContent
).
toBe
(
''
);
expect
(
component
.
$el
.
classList
.
contains
(
'
.no-description
'
)).
toBe
(
false
);
});
});
describe
(
'
user has not access to group
'
,
()
=>
{
beforeEach
((
done
)
=>
{
GroupItemComponent
=
Vue
.
extend
(
groupItemComponent
);
store
=
new
GroupsStore
();
group1
.
permissions
.
human_group_access
=
null
;
group
=
store
.
decorateGroup
(
group1
);
component
=
new
GroupItemComponent
({
propsData
:
{
group
,
},
}).
$mount
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
afterEach
(()
=>
{
component
.
$destroy
();
});
it
(
'
should not display access type
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
.access-type
'
)).
toBeNull
();
});
});
});
spec/javascripts/groups/groups_spec.js
deleted
100644 → 0
View file @
67815272
import
Vue
from
'
vue
'
;
import
eventHub
from
'
~/groups/event_hub
'
;
import
groupFolderComponent
from
'
~/groups/components/group_folder.vue
'
;
import
groupItemComponent
from
'
~/groups/components/group_item.vue
'
;
import
groupsComponent
from
'
~/groups/components/groups.vue
'
;
import
GroupsStore
from
'
~/groups/stores/groups_store
'
;
import
{
groupsData
}
from
'
./mock_data
'
;
describe
(
'
Groups Component
'
,
()
=>
{
let
GroupsComponent
;
let
store
;
let
component
;
let
groups
;
beforeEach
((
done
)
=>
{
Vue
.
component
(
'
group-folder
'
,
groupFolderComponent
);
Vue
.
component
(
'
group-item
'
,
groupItemComponent
);
store
=
new
GroupsStore
();
groups
=
store
.
setGroups
(
groupsData
.
groups
);
store
.
storePagination
(
groupsData
.
pagination
);
GroupsComponent
=
Vue
.
extend
(
groupsComponent
);
component
=
new
GroupsComponent
({
propsData
:
{
groups
:
store
.
state
.
groups
,
pageInfo
:
store
.
state
.
pageInfo
,
},
}).
$mount
();
Vue
.
nextTick
(()
=>
{
done
();
});
});
afterEach
(()
=>
{
component
.
$destroy
();
});
describe
(
'
with data
'
,
()
=>
{
it
(
'
should render a list of groups
'
,
()
=>
{
expect
(
component
.
$el
.
classList
.
contains
(
'
groups-list-tree-container
'
)).
toBe
(
true
);
expect
(
component
.
$el
.
querySelector
(
'
#group-12
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
#group-1119
'
)).
toBeDefined
();
expect
(
component
.
$el
.
querySelector
(
'
#group-1120
'
)).
toBeDefined
();
});
it
(
'
should respect the order of groups
'
,
()
=>
{
const
wrap
=
component
.
$el
.
querySelector
(
'
.groups-list-tree-container > .group-list-tree
'
);
expect
(
wrap
.
querySelector
(
'
.group-row:nth-child(1)
'
).
id
).
toBe
(
'
group-12
'
);
expect
(
wrap
.
querySelector
(
'
.group-row:nth-child(2)
'
).
id
).
toBe
(
'
group-1119
'
);
});
it
(
'
should render group and its subgroup
'
,
()
=>
{
const
lists
=
component
.
$el
.
querySelectorAll
(
'
.group-list-tree
'
);
expect
(
lists
.
length
).
toBe
(
3
);
// one parent and two subgroups
expect
(
lists
[
0
].
querySelector
(
'
#group-1119
'
).
classList
.
contains
(
'
is-open
'
)).
toBe
(
true
);
expect
(
lists
[
0
].
querySelector
(
'
#group-1119
'
).
classList
.
contains
(
'
has-subgroups
'
)).
toBe
(
true
);
expect
(
lists
[
2
].
querySelector
(
'
#group-1120
'
).
textContent
).
toContain
(
groups
.
id1119
.
subGroups
.
id1120
.
name
);
});
it
(
'
should render group identicon when group avatar is not present
'
,
()
=>
{
const
avatar
=
component
.
$el
.
querySelector
(
'
#group-12 .avatar-container .avatar
'
);
expect
(
avatar
.
nodeName
).
toBe
(
'
DIV
'
);
expect
(
avatar
.
classList
.
contains
(
'
identicon
'
)).
toBeTruthy
();
expect
(
avatar
.
getAttribute
(
'
style
'
).
indexOf
(
'
background-color
'
)
>
-
1
).
toBeTruthy
();
});
it
(
'
should render group avatar when group avatar is present
'
,
()
=>
{
const
avatar
=
component
.
$el
.
querySelector
(
'
#group-1120 .avatar-container .avatar
'
);
expect
(
avatar
.
nodeName
).
toBe
(
'
IMG
'
);
expect
(
avatar
.
classList
.
contains
(
'
identicon
'
)).
toBeFalsy
();
});
it
(
'
should remove prefix of parent group
'
,
()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
#group-12 #group-1128 .title
'
).
textContent
).
toContain
(
'
level2 / level3 / level4
'
);
});
it
(
'
should remove the group after leaving the group
'
,
(
done
)
=>
{
spyOn
(
window
,
'
confirm
'
).
and
.
returnValue
(
true
);
eventHub
.
$on
(
'
leaveGroup
'
,
(
group
,
collection
)
=>
{
store
.
removeGroup
(
group
,
collection
);
});
component
.
$el
.
querySelector
(
'
#group-12 .leave-group
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
component
.
$el
.
querySelector
(
'
#group-12
'
)).
toBeNull
();
done
();
});
});
});
});
spec/javascripts/groups/mock_data.js
View file @
de553961
const
group1
=
{
id
:
12
,
name
:
'
level1
'
,
path
:
'
level1
'
,
description
:
'
foo
'
,
visibility
:
'
public
'
,
avatar_url
:
null
,
web_url
:
'
http://localhost:3000/groups/level1
'
,
group_path
:
'
/level1
'
,
full_name
:
'
level1
'
,
full_path
:
'
level1
'
,
parent_id
:
null
,
created_at
:
'
2017-05-15T19:01:23.670Z
'
,
updated_at
:
'
2017-05-15T19:01:23.670Z
'
,
number_projects_with_delimiter
:
'
1
'
,
number_users_with_delimiter
:
'
1
'
,
has_subgroups
:
true
,
permissions
:
{
human_group_access
:
'
Master
'
,
},
export
const
mockEndpoint
=
'
/dashboard/groups.json
'
;
export
const
ITEM_TYPE
=
{
PROJECT
:
'
project
'
,
GROUP
:
'
group
'
,
};
// This group has no direct parent, should be placed as subgroup of group1
const
group14
=
{
id
:
1128
,
name
:
'
level4
'
,
path
:
'
level4
'
,
description
:
'
foo
'
,
visibility
:
'
public
'
,
avatar_url
:
null
,
web_url
:
'
http://localhost:3000/groups/level1/level2/level3/level4
'
,
group_path
:
'
/level1/level2/level3/level4
'
,
full_name
:
'
level1 / level2 / level3 / level4
'
,
full_path
:
'
level1/level2/level3/level4
'
,
parent_id
:
1127
,
created_at
:
'
2017-05-15T19:02:01.645Z
'
,
updated_at
:
'
2017-05-15T19:02:01.645Z
'
,
number_projects_with_delimiter
:
'
1
'
,
number_users_with_delimiter
:
'
1
'
,
has_subgroups
:
true
,
permissions
:
{
human_group_access
:
'
Master
'
,
},
export
const
GROUP_VISIBILITY_TYPE
=
{
public
:
'
Public - The group and any public projects can be viewed without any authentication.
'
,
internal
:
'
Internal - The group and any internal projects can be viewed by any logged in user.
'
,
private
:
'
Private - The group and its projects can only be viewed by members.
'
,
};
const
group2
=
{
id
:
1119
,
name
:
'
devops
'
,
path
:
'
devops
'
,
description
:
'
foo
'
,
visibility
:
'
public
'
,
avatar_url
:
null
,
web_url
:
'
http://localhost:3000/groups/devops
'
,
group_path
:
'
/devops
'
,
full_name
:
'
devops
'
,
full_path
:
'
devops
'
,
parent_id
:
null
,
created_at
:
'
2017-05-11T19:35:09.635Z
'
,
updated_at
:
'
2017-05-11T19:35:09.635Z
'
,
number_projects_with_delimiter
:
'
1
'
,
number_users_with_delimiter
:
'
1
'
,
has_subgroups
:
true
,
permissions
:
{
human_group_access
:
'
Master
'
,
},
export
const
PROJECT_VISIBILITY_TYPE
=
{
public
:
'
Public - The project can be accessed without any authentication.
'
,
internal
:
'
Internal - The project can be accessed by any logged in user.
'
,
private
:
'
Private - Project access must be granted explicitly to each user.
'
,
};
export
const
VISIBILITY_TYPE_ICON
=
{
public
:
'
fa-globe
'
,
internal
:
'
fa-shield
'
,
private
:
'
fa-lock
'
,
};
const
group21
=
{
id
:
1120
,
name
:
'
chef
'
,
path
:
'
chef
'
,
description
:
'
foo
'
,
export
const
mockParentGroupItem
=
{
id
:
55
,
name
:
'
hardware
'
,
description
:
''
,
visibility
:
'
public
'
,
avatar_url
:
'
/uploads/-/system/group/avatar/2/GitLab.png
'
,
web_url
:
'
http://localhost:3000/groups/devops/chef
'
,
group_path
:
'
/devops/chef
'
,
full_name
:
'
devops / chef
'
,
full_path
:
'
devops/chef
'
,
parent_id
:
1119
,
created_at
:
'
2017-05-11T19:51:04.060Z
'
,
updated_at
:
'
2017-05-11T19:51:04.060Z
'
,
number_projects_with_delimiter
:
'
1
'
,
number_users_with_delimiter
:
'
1
'
,
has_subgroups
:
true
,
permissions
:
{
human_group_access
:
'
Master
'
,
},
fullName
:
'
platform / hardware
'
,
relativePath
:
'
/platform/hardware
'
,
canEdit
:
true
,
type
:
'
group
'
,
avatarUrl
:
null
,
permission
:
'
Owner
'
,
editPath
:
'
/groups/platform/hardware/edit
'
,
childrenCount
:
3
,
leavePath
:
'
/groups/platform/hardware/group_members/leave
'
,
parentId
:
54
,
memberCount
:
'
1
'
,
projectCount
:
1
,
subgroupCount
:
2
,
canLeave
:
false
,
children
:
[],
isOpen
:
true
,
isChildrenLoading
:
false
,
isBeingRemoved
:
false
,
};
const
groupsData
=
{
groups
:
[
group1
,
group14
,
group2
,
group21
],
pagination
:
{
Date
:
'
Mon, 22 May 2017 22:31:52 GMT
'
,
'
X-Prev-Page
'
:
'
1
'
,
'
X-Content-Type-Options
'
:
'
nosniff
'
,
'
X-Total
'
:
'
31
'
,
'
Transfer-Encoding
'
:
'
chunked
'
,
'
X-Runtime
'
:
'
0.611144
'
,
'
X-Xss-Protection
'
:
'
1; mode=block
'
,
'
X-Request-Id
'
:
'
f5db8368-3ce5-4aa4-89d2-a125d9dead09
'
,
'
X-Ua-Compatible
'
:
'
IE=edge
'
,
'
X-Per-Page
'
:
'
20
'
,
Link
:
'
<http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="prev", <http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="first", <http://localhost:3000/dashboard/groups.json?page=2&per_page=20>; rel="last"
'
,
'
X-Next-Page
'
:
''
,
Etag
:
'
W/"a82f846947136271cdb7d55d19ef33d2"
'
,
'
X-Frame-Options
'
:
'
DENY
'
,
'
Content-Type
'
:
'
application/json; charset=utf-8
'
,
'
Cache-Control
'
:
'
max-age=0, private, must-revalidate
'
,
'
X-Total-Pages
'
:
'
2
'
,
'
X-Page
'
:
'
2
'
,
export
const
mockRawChildren
=
[
{
id
:
57
,
name
:
'
bsp
'
,
description
:
''
,
visibility
:
'
public
'
,
full_name
:
'
platform / hardware / bsp
'
,
relative_path
:
'
/platform/hardware/bsp
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/platform/hardware/bsp/edit
'
,
children_count
:
6
,
leave_path
:
'
/groups/platform/hardware/bsp/group_members/leave
'
,
parent_id
:
55
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
4
,
subgroup_count
:
2
,
can_leave
:
false
,
children
:
[],
},
];
export
const
mockChildren
=
[
{
id
:
57
,
name
:
'
bsp
'
,
description
:
''
,
visibility
:
'
public
'
,
fullName
:
'
platform / hardware / bsp
'
,
relativePath
:
'
/platform/hardware/bsp
'
,
canEdit
:
true
,
type
:
'
group
'
,
avatarUrl
:
null
,
permission
:
'
Owner
'
,
editPath
:
'
/groups/platform/hardware/bsp/edit
'
,
childrenCount
:
6
,
leavePath
:
'
/groups/platform/hardware/bsp/group_members/leave
'
,
parentId
:
55
,
memberCount
:
'
1
'
,
projectCount
:
4
,
subgroupCount
:
2
,
canLeave
:
false
,
children
:
[],
isOpen
:
true
,
isChildrenLoading
:
false
,
isBeingRemoved
:
false
,
},
];
export
const
mockGroups
=
[
{
id
:
75
,
name
:
'
test-group
'
,
description
:
''
,
visibility
:
'
public
'
,
full_name
:
'
test-group
'
,
relative_path
:
'
/test-group
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/test-group/edit
'
,
children_count
:
2
,
leave_path
:
'
/groups/test-group/group_members/leave
'
,
parent_id
:
null
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
2
,
subgroup_count
:
0
,
can_leave
:
false
,
},
{
id
:
67
,
name
:
'
open-source
'
,
description
:
''
,
visibility
:
'
private
'
,
full_name
:
'
open-source
'
,
relative_path
:
'
/open-source
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/open-source/edit
'
,
children_count
:
0
,
leave_path
:
'
/groups/open-source/group_members/leave
'
,
parent_id
:
null
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
0
,
subgroup_count
:
0
,
can_leave
:
false
,
},
{
id
:
54
,
name
:
'
platform
'
,
description
:
''
,
visibility
:
'
public
'
,
full_name
:
'
platform
'
,
relative_path
:
'
/platform
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/platform/edit
'
,
children_count
:
1
,
leave_path
:
'
/groups/platform/group_members/leave
'
,
parent_id
:
null
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
0
,
subgroup_count
:
1
,
can_leave
:
false
,
},
{
id
:
5
,
name
:
'
H5bp
'
,
description
:
'
Minus dolor consequuntur qui nam recusandae quam incidunt.
'
,
visibility
:
'
public
'
,
full_name
:
'
H5bp
'
,
relative_path
:
'
/h5bp
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/h5bp/edit
'
,
children_count
:
1
,
leave_path
:
'
/groups/h5bp/group_members/leave
'
,
parent_id
:
null
,
number_users_with_delimiter
:
'
5
'
,
project_count
:
1
,
subgroup_count
:
0
,
can_leave
:
false
,
},
{
id
:
4
,
name
:
'
Twitter
'
,
description
:
'
Deserunt hic nostrum placeat veniam.
'
,
visibility
:
'
public
'
,
full_name
:
'
Twitter
'
,
relative_path
:
'
/twitter
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/twitter/edit
'
,
children_count
:
2
,
leave_path
:
'
/groups/twitter/group_members/leave
'
,
parent_id
:
null
,
number_users_with_delimiter
:
'
5
'
,
project_count
:
2
,
subgroup_count
:
0
,
can_leave
:
false
,
},
{
id
:
3
,
name
:
'
Documentcloud
'
,
description
:
'
Consequatur saepe totam ea pariatur maxime.
'
,
visibility
:
'
public
'
,
full_name
:
'
Documentcloud
'
,
relative_path
:
'
/documentcloud
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/documentcloud/edit
'
,
children_count
:
1
,
leave_path
:
'
/groups/documentcloud/group_members/leave
'
,
parent_id
:
null
,
number_users_with_delimiter
:
'
5
'
,
project_count
:
1
,
subgroup_count
:
0
,
can_leave
:
false
,
},
{
id
:
2
,
name
:
'
Gitlab Org
'
,
description
:
'
Debitis ea quas aperiam velit doloremque ab.
'
,
visibility
:
'
public
'
,
full_name
:
'
Gitlab Org
'
,
relative_path
:
'
/gitlab-org
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
'
/uploads/-/system/group/avatar/2/GitLab.png
'
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/gitlab-org/edit
'
,
children_count
:
4
,
leave_path
:
'
/groups/gitlab-org/group_members/leave
'
,
parent_id
:
null
,
number_users_with_delimiter
:
'
5
'
,
project_count
:
4
,
subgroup_count
:
0
,
can_leave
:
false
,
},
];
export
const
mockSearchedGroups
=
[
{
id
:
55
,
name
:
'
hardware
'
,
description
:
''
,
visibility
:
'
public
'
,
full_name
:
'
platform / hardware
'
,
relative_path
:
'
/platform/hardware
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/platform/hardware/edit
'
,
children_count
:
3
,
leave_path
:
'
/groups/platform/hardware/group_members/leave
'
,
parent_id
:
54
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
1
,
subgroup_count
:
2
,
can_leave
:
false
,
children
:
[
{
id
:
57
,
name
:
'
bsp
'
,
description
:
''
,
visibility
:
'
public
'
,
full_name
:
'
platform / hardware / bsp
'
,
relative_path
:
'
/platform/hardware/bsp
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/platform/hardware/bsp/edit
'
,
children_count
:
6
,
leave_path
:
'
/groups/platform/hardware/bsp/group_members/leave
'
,
parent_id
:
55
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
4
,
subgroup_count
:
2
,
can_leave
:
false
,
children
:
[
{
id
:
60
,
name
:
'
kernel
'
,
description
:
''
,
visibility
:
'
public
'
,
full_name
:
'
platform / hardware / bsp / kernel
'
,
relative_path
:
'
/platform/hardware/bsp/kernel
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/platform/hardware/bsp/kernel/edit
'
,
children_count
:
1
,
leave_path
:
'
/groups/platform/hardware/bsp/kernel/group_members/leave
'
,
parent_id
:
57
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
0
,
subgroup_count
:
1
,
can_leave
:
false
,
children
:
[
{
id
:
61
,
name
:
'
common
'
,
description
:
''
,
visibility
:
'
public
'
,
full_name
:
'
platform / hardware / bsp / kernel / common
'
,
relative_path
:
'
/platform/hardware/bsp/kernel/common
'
,
can_edit
:
true
,
type
:
'
group
'
,
avatar_url
:
null
,
permission
:
'
Owner
'
,
edit_path
:
'
/groups/platform/hardware/bsp/kernel/common/edit
'
,
children_count
:
2
,
leave_path
:
'
/groups/platform/hardware/bsp/kernel/common/group_members/leave
'
,
parent_id
:
60
,
number_users_with_delimiter
:
'
1
'
,
project_count
:
2
,
subgroup_count
:
0
,
can_leave
:
false
,
children
:
[
{
id
:
17
,
name
:
'
v4.4
'
,
description
:
'
Voluptatem qui ea error aperiam veritatis doloremque consequatur temporibus.
'
,
visibility
:
'
public
'
,
full_name
:
'
platform / hardware / bsp / kernel / common / v4.4
'
,
relative_path
:
'
/platform/hardware/bsp/kernel/common/v4.4
'
,
can_edit
:
true
,
type
:
'
project
'
,
avatar_url
:
null
,
permission
:
null
,
edit_path
:
'
/platform/hardware/bsp/kernel/common/v4.4/edit
'
,
star_count
:
0
,
},
{
id
:
16
,
name
:
'
v4.1
'
,
description
:
'
Rerum expedita voluptatem doloribus neque ducimus ut hic.
'
,
visibility
:
'
public
'
,
full_name
:
'
platform / hardware / bsp / kernel / common / v4.1
'
,
relative_path
:
'
/platform/hardware/bsp/kernel/common/v4.1
'
,
can_edit
:
true
,
type
:
'
project
'
,
avatar_url
:
null
,
permission
:
null
,
edit_path
:
'
/platform/hardware/bsp/kernel/common/v4.1/edit
'
,
star_count
:
0
,
},
],
},
],
},
],
},
],
},
];
export
const
mockRawPageInfo
=
{
'
x-per-page
'
:
10
,
'
x-page
'
:
10
,
'
x-total
'
:
10
,
'
x-total-pages
'
:
10
,
'
x-next-page
'
:
10
,
'
x-prev-page
'
:
10
,
};
export
{
groupsData
,
group1
};
export
const
mockPageInfo
=
{
perPage
:
10
,
page
:
10
,
total
:
10
,
totalPages
:
10
,
nextPage
:
10
,
prevPage
:
10
,
};
spec/javascripts/groups/service/groups_service_spec.js
0 → 100644
View file @
de553961
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
import
GroupsService
from
'
~/groups/service/groups_service
'
;
import
{
mockEndpoint
,
mockParentGroupItem
}
from
'
../mock_data
'
;
Vue
.
use
(
VueResource
);
describe
(
'
GroupsService
'
,
()
=>
{
let
service
;
beforeEach
(()
=>
{
service
=
new
GroupsService
(
mockEndpoint
);
});
describe
(
'
getGroups
'
,
()
=>
{
it
(
'
should return promise for `GET` request on provided endpoint
'
,
()
=>
{
spyOn
(
service
.
groups
,
'
get
'
).
and
.
stub
();
const
queryParams
=
{
page
:
2
,
filter
:
'
git
'
,
sort
:
'
created_asc
'
,
};
service
.
getGroups
(
55
,
2
,
'
git
'
,
'
created_asc
'
);
expect
(
service
.
groups
.
get
).
toHaveBeenCalledWith
({
parent_id
:
55
});
service
.
getGroups
(
null
,
2
,
'
git
'
,
'
created_asc
'
);
expect
(
service
.
groups
.
get
).
toHaveBeenCalledWith
(
queryParams
);
});
});
describe
(
'
leaveGroup
'
,
()
=>
{
it
(
'
should return promise for `DELETE` request on provided endpoint
'
,
()
=>
{
spyOn
(
Vue
.
http
,
'
delete
'
).
and
.
stub
();
service
.
leaveGroup
(
mockParentGroupItem
.
leavePath
);
expect
(
Vue
.
http
.
delete
).
toHaveBeenCalledWith
(
mockParentGroupItem
.
leavePath
);
});
});
});
spec/javascripts/groups/store/groups_store_spec.js
0 → 100644
View file @
de553961
import
GroupsStore
from
'
~/groups/store/groups_store
'
;
import
{
mockGroups
,
mockSearchedGroups
,
mockParentGroupItem
,
mockRawChildren
,
mockRawPageInfo
,
}
from
'
../mock_data
'
;
describe
(
'
ProjectsStore
'
,
()
=>
{
describe
(
'
constructor
'
,
()
=>
{
it
(
'
should initialize default state
'
,
()
=>
{
let
store
;
store
=
new
GroupsStore
();
expect
(
Object
.
keys
(
store
.
state
).
length
).
toBe
(
2
);
expect
(
Array
.
isArray
(
store
.
state
.
groups
)).
toBeTruthy
();
expect
(
Object
.
keys
(
store
.
state
.
pageInfo
).
length
).
toBe
(
0
);
expect
(
store
.
hideProjects
).
not
.
toBeDefined
();
store
=
new
GroupsStore
(
true
);
expect
(
store
.
hideProjects
).
toBeTruthy
();
});
});
describe
(
'
setGroups
'
,
()
=>
{
it
(
'
should set groups to state
'
,
()
=>
{
const
store
=
new
GroupsStore
();
spyOn
(
store
,
'
formatGroupItem
'
).
and
.
callThrough
();
store
.
setGroups
(
mockGroups
);
expect
(
store
.
state
.
groups
.
length
).
toBe
(
mockGroups
.
length
);
expect
(
store
.
formatGroupItem
).
toHaveBeenCalledWith
(
jasmine
.
any
(
Object
));
expect
(
Object
.
keys
(
store
.
state
.
groups
[
0
]).
indexOf
(
'
fullName
'
)
>
-
1
).
toBeTruthy
();
});
});
describe
(
'
setSearchedGroups
'
,
()
=>
{
it
(
'
should set searched groups to state
'
,
()
=>
{
const
store
=
new
GroupsStore
();
spyOn
(
store
,
'
formatGroupItem
'
).
and
.
callThrough
();
store
.
setSearchedGroups
(
mockSearchedGroups
);
expect
(
store
.
state
.
groups
.
length
).
toBe
(
mockSearchedGroups
.
length
);
expect
(
store
.
formatGroupItem
).
toHaveBeenCalledWith
(
jasmine
.
any
(
Object
));
expect
(
Object
.
keys
(
store
.
state
.
groups
[
0
]).
indexOf
(
'
fullName
'
)
>
-
1
).
toBeTruthy
();
expect
(
Object
.
keys
(
store
.
state
.
groups
[
0
].
children
[
0
]).
indexOf
(
'
fullName
'
)
>
-
1
).
toBeTruthy
();
});
});
describe
(
'
setGroupChildren
'
,
()
=>
{
it
(
'
should set children to group item in state
'
,
()
=>
{
const
store
=
new
GroupsStore
();
spyOn
(
store
,
'
formatGroupItem
'
).
and
.
callThrough
();
store
.
setGroupChildren
(
mockParentGroupItem
,
mockRawChildren
);
expect
(
store
.
formatGroupItem
).
toHaveBeenCalledWith
(
jasmine
.
any
(
Object
));
expect
(
mockParentGroupItem
.
children
.
length
).
toBe
(
1
);
expect
(
Object
.
keys
(
mockParentGroupItem
.
children
[
0
]).
indexOf
(
'
fullName
'
)
>
-
1
).
toBeTruthy
();
expect
(
mockParentGroupItem
.
isOpen
).
toBeTruthy
();
expect
(
mockParentGroupItem
.
isChildrenLoading
).
toBeFalsy
();
});
});
describe
(
'
setPaginationInfo
'
,
()
=>
{
it
(
'
should parse and set pagination info in state
'
,
()
=>
{
const
store
=
new
GroupsStore
();
store
.
setPaginationInfo
(
mockRawPageInfo
);
expect
(
store
.
state
.
pageInfo
.
perPage
).
toBe
(
10
);
expect
(
store
.
state
.
pageInfo
.
page
).
toBe
(
10
);
expect
(
store
.
state
.
pageInfo
.
total
).
toBe
(
10
);
expect
(
store
.
state
.
pageInfo
.
totalPages
).
toBe
(
10
);
expect
(
store
.
state
.
pageInfo
.
nextPage
).
toBe
(
10
);
expect
(
store
.
state
.
pageInfo
.
previousPage
).
toBe
(
10
);
});
});
describe
(
'
formatGroupItem
'
,
()
=>
{
it
(
'
should parse group item object and return updated object
'
,
()
=>
{
let
store
;
let
updatedGroupItem
;
store
=
new
GroupsStore
();
updatedGroupItem
=
store
.
formatGroupItem
(
mockRawChildren
[
0
]);
expect
(
Object
.
keys
(
updatedGroupItem
).
indexOf
(
'
fullName
'
)
>
-
1
).
toBeTruthy
();
expect
(
updatedGroupItem
.
childrenCount
).
toBe
(
mockRawChildren
[
0
].
children_count
);
expect
(
updatedGroupItem
.
isChildrenLoading
).
toBe
(
false
);
expect
(
updatedGroupItem
.
isBeingRemoved
).
toBe
(
false
);
store
=
new
GroupsStore
(
true
);
updatedGroupItem
=
store
.
formatGroupItem
(
mockRawChildren
[
0
]);
expect
(
Object
.
keys
(
updatedGroupItem
).
indexOf
(
'
fullName
'
)
>
-
1
).
toBeTruthy
();
expect
(
updatedGroupItem
.
childrenCount
).
toBe
(
mockRawChildren
[
0
].
subgroup_count
);
});
});
describe
(
'
removeGroup
'
,
()
=>
{
it
(
'
should remove children from group item in state
'
,
()
=>
{
const
store
=
new
GroupsStore
();
const
rawParentGroup
=
Object
.
assign
({},
mockGroups
[
0
]);
const
rawChildGroup
=
Object
.
assign
({},
mockGroups
[
1
]);
store
.
setGroups
([
rawParentGroup
]);
store
.
setGroupChildren
(
store
.
state
.
groups
[
0
],
[
rawChildGroup
]);
const
childItem
=
store
.
state
.
groups
[
0
].
children
[
0
];
store
.
removeGroup
(
childItem
,
store
.
state
.
groups
[
0
]);
expect
(
store
.
state
.
groups
[
0
].
children
.
length
).
toBe
(
0
);
});
});
});
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