BigW Consortium Gitlab
Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
G
gitlab-ce
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Forest Godfrey
gitlab-ce
Commits
6e4949db
Commit
6e4949db
authored
Sep 05, 2017
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch '35010-projects-nav-dropdown' into 'master'
Add dropdown to Projects nav item Closes #35010 See merge request !13866
parents
75d56cec
de10551e
Show whitespace changes
Inline
Side-by-side
Showing
33 changed files
with
1916 additions
and
13 deletions
+1916
-13
api.js
app/assets/javascripts/api.js
+2
-1
index.js
app/assets/javascripts/commons/index.js
+1
-0
vue.js
app/assets/javascripts/commons/vue.js
+0
-1
main.js
app/assets/javascripts/main.js
+1
-0
app.vue
app/assets/javascripts/projects_dropdown/components/app.vue
+157
-0
projects_list_frequent.vue
...s/projects_dropdown/components/projects_list_frequent.vue
+57
-0
projects_list_item.vue
...ripts/projects_dropdown/components/projects_list_item.vue
+96
-0
projects_list_search.vue
...pts/projects_dropdown/components/projects_list_search.vue
+63
-0
search.vue
...ssets/javascripts/projects_dropdown/components/search.vue
+64
-0
constants.js
app/assets/javascripts/projects_dropdown/constants.js
+10
-0
event_hub.js
app/assets/javascripts/projects_dropdown/event_hub.js
+3
-0
index.js
app/assets/javascripts/projects_dropdown/index.js
+68
-0
projects_service.js
...javascripts/projects_dropdown/service/projects_service.js
+132
-0
projects_store.js
...ets/javascripts/projects_dropdown/store/projects_store.js
+33
-0
identicon.vue
app/assets/javascripts/vue_shared/components/identicon.vue
+7
-1
common.scss
app/assets/stylesheets/framework/common.scss
+1
-0
dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+149
-0
_new_dashboard.html.haml
app/views/layouts/nav/_new_dashboard.html.haml
+10
-2
_show.html.haml
app/views/layouts/nav/projects_dropdown/_show.html.haml
+15
-0
35010-projects-nav-dropdown.yml
changelogs/unreleased/35010-projects-nav-dropdown.yml
+5
-0
webpack.config.js
config/webpack.config.js
+1
-1
gitlab.pot
locale/gitlab.pot
+32
-2
api_spec.js
spec/javascripts/api_spec.js
+4
-2
project_title_spec.js
spec/javascripts/project_title_spec.js
+2
-1
app_spec.js
spec/javascripts/projects_dropdown/components/app_spec.js
+348
-0
projects_list_frequent_spec.js
...ojects_dropdown/components/projects_list_frequent_spec.js
+72
-0
projects_list_item_spec.js
...s/projects_dropdown/components/projects_list_item_spec.js
+65
-0
projects_list_search_spec.js
...projects_dropdown/components/projects_list_search_spec.js
+84
-0
search_spec.js
spec/javascripts/projects_dropdown/components/search_spec.js
+101
-0
mock_data.js
spec/javascripts/projects_dropdown/mock_data.js
+96
-0
projects_service_spec.js
...cripts/projects_dropdown/service/projects_service_spec.js
+178
-0
projects_store_spec.js
...avascripts/projects_dropdown/store/projects_store_spec.js
+41
-0
identicon_spec.js
spec/javascripts/vue_shared/components/identicon_spec.js
+18
-2
No files found.
app/assets/javascripts/api.js
View file @
6e4949db
...
...
@@ -5,7 +5,7 @@ const Api = {
groupPath
:
'/api/:version/groups/:id.json'
,
namespacesPath
:
'/api/:version/namespaces.json'
,
groupProjectsPath
:
'/api/:version/groups/:id/projects.json'
,
projectsPath
:
'/api/:version/projects.json
?simple=true
'
,
projectsPath
:
'/api/:version/projects.json'
,
labelsPath
:
'/:namespace_path/:project_path/labels'
,
licensePath
:
'/api/:version/templates/licenses/:key'
,
gitignorePath
:
'/api/:version/templates/gitignores/:key'
,
...
...
@@ -58,6 +58,7 @@ const Api = {
const
defaults
=
{
search
:
query
,
per_page
:
20
,
simple
:
true
,
};
if
(
gon
.
current_user_id
)
{
...
...
app/assets/javascripts/commons/index.js
View file @
6e4949db
...
...
@@ -2,3 +2,4 @@ import 'underscore';
import
'./polyfills'
;
import
'./jquery'
;
import
'./bootstrap'
;
import
'./vue'
;
app/assets/javascripts/
vue_shared/common_
vue.js
→
app/assets/javascripts/
commons/
vue.js
View file @
6e4949db
import
Vue
from
'vue'
;
import
'./vue_resource_interceptor'
;
if
(
process
.
env
.
NODE_ENV
!==
'production'
)
{
Vue
.
config
.
productionTip
=
false
;
...
...
app/assets/javascripts/main.js
View file @
6e4949db
...
...
@@ -132,6 +132,7 @@ import './project_new';
import
'./project_select'
;
import
'./project_show'
;
import
'./project_variables'
;
import
'./projects_dropdown'
;
import
'./projects_list'
;
import
'./syntax_highlight'
;
import
'./render_math'
;
...
...
app/assets/javascripts/projects_dropdown/components/app.vue
0 → 100644
View file @
6e4949db
<
script
>
import
bs
from
'../../breakpoints'
;
import
eventHub
from
'../event_hub'
;
import
loadingIcon
from
'../../vue_shared/components/loading_icon.vue'
;
import
projectsListFrequent
from
'./projects_list_frequent.vue'
;
import
projectsListSearch
from
'./projects_list_search.vue'
;
import
search
from
'./search.vue'
;
export
default
{
components
:
{
search
,
loadingIcon
,
projectsListFrequent
,
projectsListSearch
,
},
props
:
{
currentProject
:
{
type
:
Object
,
required
:
true
,
},
store
:
{
type
:
Object
,
required
:
true
,
},
service
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
isLoadingProjects
:
false
,
isFrequentsListVisible
:
false
,
isSearchListVisible
:
false
,
isLocalStorageFailed
:
false
,
isSearchFailed
:
false
,
searchQuery
:
''
,
};
},
computed
:
{
frequentProjects
()
{
return
this
.
store
.
getFrequentProjects
();
},
searchProjects
()
{
return
this
.
store
.
getSearchedProjects
();
},
},
methods
:
{
toggleFrequentProjectsList
(
state
)
{
this
.
isLoadingProjects
=
!
state
;
this
.
isSearchListVisible
=
!
state
;
this
.
isFrequentsListVisible
=
state
;
},
toggleSearchProjectsList
(
state
)
{
this
.
isLoadingProjects
=
!
state
;
this
.
isFrequentsListVisible
=
!
state
;
this
.
isSearchListVisible
=
state
;
},
toggleLoader
(
state
)
{
this
.
isFrequentsListVisible
=
!
state
;
this
.
isSearchListVisible
=
!
state
;
this
.
isLoadingProjects
=
state
;
},
fetchFrequentProjects
()
{
const
screenSize
=
bs
.
getBreakpointSize
();
if
(
this
.
searchQuery
&&
(
screenSize
!==
'sm'
&&
screenSize
!==
'xs'
))
{
this
.
toggleSearchProjectsList
(
true
);
}
else
{
this
.
toggleLoader
(
true
);
this
.
isLocalStorageFailed
=
false
;
const
projects
=
this
.
service
.
getFrequentProjects
();
if
(
projects
)
{
this
.
toggleFrequentProjectsList
(
true
);
this
.
store
.
setFrequentProjects
(
projects
);
}
else
{
this
.
isLocalStorageFailed
=
true
;
this
.
toggleFrequentProjectsList
(
true
);
this
.
store
.
setFrequentProjects
([]);
}
}
},
fetchSearchedProjects
(
searchQuery
)
{
this
.
searchQuery
=
searchQuery
;
this
.
toggleLoader
(
true
);
this
.
service
.
getSearchedProjects
(
this
.
searchQuery
)
.
then
(
res
=>
res
.
json
())
.
then
((
results
)
=>
{
this
.
toggleSearchProjectsList
(
true
);
this
.
store
.
setSearchedProjects
(
results
);
})
.
catch
(()
=>
{
this
.
isSearchFailed
=
true
;
this
.
toggleSearchProjectsList
(
true
);
});
},
logCurrentProjectAccess
()
{
this
.
service
.
logProjectAccess
(
this
.
currentProject
);
},
handleSearchClear
()
{
this
.
searchQuery
=
''
;
this
.
toggleFrequentProjectsList
(
true
);
this
.
store
.
clearSearchedProjects
();
},
handleSearchFailure
()
{
this
.
isSearchFailed
=
true
;
this
.
toggleSearchProjectsList
(
true
);
},
},
created
()
{
if
(
this
.
currentProject
.
id
)
{
this
.
logCurrentProjectAccess
();
}
eventHub
.
$on
(
'dropdownOpen'
,
this
.
fetchFrequentProjects
);
eventHub
.
$on
(
'searchProjects'
,
this
.
fetchSearchedProjects
);
eventHub
.
$on
(
'searchCleared'
,
this
.
handleSearchClear
);
eventHub
.
$on
(
'searchFailed'
,
this
.
handleSearchFailure
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'dropdownOpen'
,
this
.
fetchFrequentProjects
);
eventHub
.
$off
(
'searchProjects'
,
this
.
fetchSearchedProjects
);
eventHub
.
$off
(
'searchCleared'
,
this
.
handleSearchClear
);
eventHub
.
$off
(
'searchFailed'
,
this
.
handleSearchFailure
);
},
};
</
script
>
<
template
>
<div>
<search/>
<loading-icon
class=
"loading-animation prepend-top-20"
size=
"2"
v-if=
"isLoadingProjects"
:label=
"s__('ProjectsDropdown|Loading projects')"
/>
<div
class=
"section-header"
v-if=
"isFrequentsListVisible"
>
{{
s__
(
'ProjectsDropdown|Frequently visited'
)
}}
</div>
<projects-list-frequent
v-if=
"isFrequentsListVisible"
:local-storage-failed=
"isLocalStorageFailed"
:projects=
"frequentProjects"
/>
<projects-list-search
v-if=
"isSearchListVisible"
:search-failed=
"isSearchFailed"
:matcher=
"searchQuery"
:projects=
"searchProjects"
/>
</div>
</
template
>
app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
0 → 100644
View file @
6e4949db
<
script
>
import
{
s__
}
from
'../../locale'
;
import
projectsListItem
from
'./projects_list_item.vue'
;
export
default
{
components
:
{
projectsListItem
,
},
props
:
{
projects
:
{
type
:
Array
,
required
:
true
,
},
localStorageFailed
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
isListEmpty
()
{
return
this
.
projects
.
length
===
0
;
},
listEmptyMessage
()
{
return
this
.
localStorageFailed
?
s__
(
'ProjectsDropdown|This feature requires browser localStorage support'
)
:
s__
(
'ProjectsDropdown|Projects you visit often will appear here'
);
},
},
};
</
script
>
<
template
>
<div
class=
"projects-list-frequent-container"
>
<ul
class=
"list-unstyled"
>
<li
class=
"section-empty"
v-if=
"isListEmpty"
>
{{
listEmptyMessage
}}
</li>
<projects-list-item
v-else
v-for=
"(project, index) in projects"
:key=
"index"
:project-id=
"project.id"
:project-name=
"project.name"
:namespace=
"project.namespace"
:web-url=
"project.webUrl"
:avatar-url=
"project.avatarUrl"
/>
</ul>
</div>
</
template
>
app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
0 → 100644
View file @
6e4949db
<
script
>
import
identicon
from
'../../vue_shared/components/identicon.vue'
;
export
default
{
components
:
{
identicon
,
},
props
:
{
matcher
:
{
type
:
String
,
required
:
false
,
},
projectId
:
{
type
:
Number
,
required
:
true
,
},
projectName
:
{
type
:
String
,
required
:
true
,
},
namespace
:
{
type
:
String
,
required
:
true
,
},
webUrl
:
{
type
:
String
,
required
:
true
,
},
avatarUrl
:
{
required
:
true
,
validator
(
value
)
{
return
value
===
null
||
typeof
value
===
'string'
;
},
},
},
computed
:
{
hasAvatar
()
{
return
this
.
avatarUrl
!==
null
;
},
highlightedProjectName
()
{
if
(
this
.
matcher
)
{
const
matcherRegEx
=
new
RegExp
(
this
.
matcher
,
'gi'
);
const
matches
=
this
.
projectName
.
match
(
matcherRegEx
);
if
(
matches
&&
matches
.
length
>
0
)
{
return
this
.
projectName
.
replace
(
matches
[
0
],
`<b>
${
matches
[
0
]}
</b>`
);
}
}
return
this
.
projectName
;
},
},
};
</
script
>
<
template
>
<li
class=
"projects-list-item-container"
>
<a
class=
"clearfix"
:href=
"webUrl"
>
<div
class=
"project-item-avatar-container"
>
<img
v-if=
"hasAvatar"
class=
"avatar s32"
:src=
"avatarUrl"
/>
<identicon
v-else
size-class=
"s32"
:entity-id=
projectId
:entity-name=
"projectName"
/>
</div>
<div
class=
"project-item-metadata-container"
>
<div
class=
"project-title"
:title=
"projectName"
v-html=
"highlightedProjectName"
>
</div>
<div
class=
"project-namespace"
:title=
"namespace"
>
{{
namespace
}}
</div>
</div>
</a>
</li>
</
template
>
app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
0 → 100644
View file @
6e4949db
<
script
>
import
{
s__
}
from
'../../locale'
;
import
projectsListItem
from
'./projects_list_item.vue'
;
export
default
{
components
:
{
projectsListItem
,
},
props
:
{
matcher
:
{
type
:
String
,
required
:
true
,
},
projects
:
{
type
:
Array
,
required
:
true
,
},
searchFailed
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
isListEmpty
()
{
return
this
.
projects
.
length
===
0
;
},
listEmptyMessage
()
{
return
this
.
searchFailed
?
s__
(
'ProjectsDropdown|Something went wrong on our end.'
)
:
s__
(
'ProjectsDropdown|No projects matched your query'
);
},
},
};
</
script
>
<
template
>
<div
class=
"projects-list-search-container"
>
<ul
class=
"list-unstyled"
>
<li
v-if=
"isListEmpty"
:class=
"
{ 'section-failure': searchFailed }"
class="section-empty"
>
{{
listEmptyMessage
}}
</li>
<projects-list-item
v-else
v-for=
"(project, index) in projects"
:key=
"index"
:project-id=
"project.id"
:project-name=
"project.name"
:namespace=
"project.namespace"
:web-url=
"project.webUrl"
:avatar-url=
"project.avatarUrl"
:matcher=
"matcher"
/>
</ul>
</div>
</
template
>
app/assets/javascripts/projects_dropdown/components/search.vue
0 → 100644
View file @
6e4949db
<
script
>
import
_
from
'underscore'
;
import
eventHub
from
'../event_hub'
;
export
default
{
data
()
{
return
{
searchQuery
:
''
,
};
},
watch
:
{
searchQuery
()
{
this
.
handleInput
();
},
},
methods
:
{
setFocus
()
{
this
.
$refs
.
search
.
focus
();
},
emitSearchEvents
()
{
if
(
this
.
searchQuery
)
{
eventHub
.
$emit
(
'searchProjects'
,
this
.
searchQuery
);
}
else
{
eventHub
.
$emit
(
'searchCleared'
);
}
},
/**
* Callback function within _.debounce is intentionally
* kept as ES5 `function() {}` instead of ES6 `() => {}`
* as it otherwise messes up function context
* and component reference is no longer accessible via `this`
*/
// eslint-disable-next-line func-names
handleInput
:
_
.
debounce
(
function
()
{
this
.
emitSearchEvents
();
},
500
),
},
mounted
()
{
eventHub
.
$on
(
'dropdownOpen'
,
this
.
setFocus
);
},
beforeDestroy
()
{
eventHub
.
$off
(
'dropdownOpen'
,
this
.
setFocus
);
},
};
</
script
>
<
template
>
<div
class=
"search-input-container hidden-xs"
>
<input
type=
"search"
class=
"form-control"
ref=
"search"
v-model=
"searchQuery"
:placeholder=
"s__('ProjectsDropdown|Search projects')"
/>
<i
v-if=
"!searchQuery"
class=
"search-icon fa fa-fw fa-search"
aria-hidden=
"true"
/>
</div>
</
template
>
app/assets/javascripts/projects_dropdown/constants.js
0 → 100644
View file @
6e4949db
export
const
FREQUENT_PROJECTS
=
{
MAX_COUNT
:
20
,
LIST_COUNT_DESKTOP
:
5
,
LIST_COUNT_MOBILE
:
3
,
ELIGIBLE_FREQUENCY
:
3
,
};
export
const
HOUR_IN_MS
=
3600000
;
export
const
STORAGE_KEY
=
'frequent-projects'
;
app/assets/javascripts/projects_dropdown/event_hub.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
export
default
new
Vue
();
app/assets/javascripts/projects_dropdown/index.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
Translate
from
'../vue_shared/translate'
;
import
eventHub
from
'./event_hub'
;
import
ProjectsService
from
'./service/projects_service'
;
import
ProjectsStore
from
'./store/projects_store'
;
import
projectsDropdownApp
from
'./components/app.vue'
;
Vue
.
use
(
Translate
);
document
.
addEventListener
(
'DOMContentLoaded'
,
()
=>
{
const
el
=
document
.
getElementById
(
'js-projects-dropdown'
);
const
navEl
=
document
.
getElementById
(
'nav-projects-dropdown'
);
// Don't do anything if element doesn't exist (No projects dropdown)
// This is for when the user accesses GitLab without logging in
if
(
!
el
||
!
navEl
)
{
return
;
}
$
(
navEl
).
on
(
'show.bs.dropdown'
,
(
e
)
=>
{
const
dropdownEl
=
$
(
e
.
currentTarget
).
find
(
'.projects-dropdown-menu'
);
dropdownEl
.
one
(
'transitionend'
,
()
=>
{
eventHub
.
$emit
(
'dropdownOpen'
);
});
});
// eslint-disable-next-line no-new
new
Vue
({
el
,
components
:
{
projectsDropdownApp
,
},
data
()
{
const
dataset
=
this
.
$options
.
el
.
dataset
;
const
store
=
new
ProjectsStore
();
const
service
=
new
ProjectsService
(
dataset
.
userName
);
const
project
=
{
id
:
Number
(
dataset
.
projectId
),
name
:
dataset
.
projectName
,
namespace
:
dataset
.
projectNamespace
,
webUrl
:
dataset
.
projectWebUrl
,
avatarUrl
:
dataset
.
projectAvatarUrl
||
null
,
lastAccessedOn
:
Date
.
now
(),
};
return
{
store
,
service
,
state
:
store
.
state
,
currentUserName
:
dataset
.
userName
,
currentProject
:
project
,
};
},
render
(
createElement
)
{
return
createElement
(
'projects-dropdown-app'
,
{
props
:
{
currentUserName
:
this
.
currentUserName
,
currentProject
:
this
.
currentProject
,
store
:
this
.
store
,
service
:
this
.
service
,
},
});
},
});
});
app/assets/javascripts/projects_dropdown/service/projects_service.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
VueResource
from
'vue-resource'
;
import
bp
from
'../../breakpoints'
;
import
Api
from
'../../api'
;
import
AccessorUtilities
from
'../../lib/utils/accessor'
;
import
{
FREQUENT_PROJECTS
,
HOUR_IN_MS
,
STORAGE_KEY
}
from
'../constants'
;
Vue
.
use
(
VueResource
);
export
default
class
ProjectsService
{
constructor
(
currentUserName
)
{
this
.
isLocalStorageAvailable
=
AccessorUtilities
.
isLocalStorageAccessSafe
();
this
.
currentUserName
=
currentUserName
;
this
.
storageKey
=
`
${
this
.
currentUserName
}
/
${
STORAGE_KEY
}
`
;
this
.
projectsPath
=
Vue
.
resource
(
Api
.
buildUrl
(
Api
.
projectsPath
));
}
getSearchedProjects
(
searchQuery
)
{
return
this
.
projectsPath
.
get
({
simple
:
false
,
per_page
:
20
,
membership
:
!!
gon
.
current_user_id
,
order_by
:
'last_activity_at'
,
search
:
searchQuery
,
});
}
getFrequentProjects
()
{
if
(
this
.
isLocalStorageAvailable
)
{
return
this
.
getTopFrequentProjects
();
}
return
null
;
}
logProjectAccess
(
project
)
{
let
matchFound
=
false
;
let
storedFrequentProjects
;
if
(
this
.
isLocalStorageAvailable
)
{
const
storedRawProjects
=
localStorage
.
getItem
(
this
.
storageKey
);
// Check if there's any frequent projects list set
if
(
!
storedRawProjects
)
{
// No frequent projects list set, set one up.
storedFrequentProjects
=
[];
storedFrequentProjects
.
push
({
...
project
,
frequency
:
1
});
}
else
{
// Check if project is already present in frequents list
// When found, update metadata of it.
storedFrequentProjects
=
JSON
.
parse
(
storedRawProjects
).
map
((
projectItem
)
=>
{
if
(
projectItem
.
id
===
project
.
id
)
{
matchFound
=
true
;
const
diff
=
Math
.
abs
(
project
.
lastAccessedOn
-
projectItem
.
lastAccessedOn
)
/
HOUR_IN_MS
;
const
updatedProject
=
{
...
project
,
frequency
:
projectItem
.
frequency
,
lastAccessedOn
:
projectItem
.
lastAccessedOn
,
};
// Check if duration since last access of this project
// is over an hour
if
(
diff
>
1
)
{
return
{
...
updatedProject
,
frequency
:
updatedProject
.
frequency
+
1
,
lastAccessedOn
:
Date
.
now
(),
};
}
return
{
...
updatedProject
,
};
}
return
projectItem
;
});
// Check whether currently logged project is present in frequents list
if
(
!
matchFound
)
{
// We always keep size of frequents collection to 20 projects
// out of which only 5 projects with
// highest value of `frequency` and most recent `lastAccessedOn`
// are shown in projects dropdown
if
(
storedFrequentProjects
.
length
===
FREQUENT_PROJECTS
.
MAX_COUNT
)
{
storedFrequentProjects
.
shift
();
// Remove an item from head of array
}
storedFrequentProjects
.
push
({
...
project
,
frequency
:
1
});
}
}
localStorage
.
setItem
(
this
.
storageKey
,
JSON
.
stringify
(
storedFrequentProjects
));
}
}
getTopFrequentProjects
()
{
const
storedFrequentProjects
=
JSON
.
parse
(
localStorage
.
getItem
(
this
.
storageKey
));
let
frequentProjectsCount
=
FREQUENT_PROJECTS
.
LIST_COUNT_DESKTOP
;
if
(
!
storedFrequentProjects
)
{
return
[];
}
if
(
bp
.
getBreakpointSize
()
===
'sm'
||
bp
.
getBreakpointSize
()
===
'xs'
)
{
frequentProjectsCount
=
FREQUENT_PROJECTS
.
LIST_COUNT_MOBILE
;
}
const
frequentProjects
=
storedFrequentProjects
.
filter
(
project
=>
project
.
frequency
>=
FREQUENT_PROJECTS
.
ELIGIBLE_FREQUENCY
);
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
frequentProjects
.
sort
((
projectA
,
projectB
)
=>
{
if
(
projectA
.
frequency
<
projectB
.
frequency
)
{
return
1
;
}
else
if
(
projectA
.
frequency
>
projectB
.
frequency
)
{
return
-
1
;
}
else
if
(
projectA
.
lastAccessedOn
<
projectB
.
lastAccessedOn
)
{
return
1
;
}
else
if
(
projectA
.
lastAccessedOn
>
projectB
.
lastAccessedOn
)
{
return
-
1
;
}
return
0
;
});
return
_
.
first
(
frequentProjects
,
frequentProjectsCount
);
}
}
app/assets/javascripts/projects_dropdown/store/projects_store.js
0 → 100644
View file @
6e4949db
export
default
class
ProjectsStore
{
constructor
()
{
this
.
state
=
{};
this
.
state
.
frequentProjects
=
[];
this
.
state
.
searchedProjects
=
[];
}
setFrequentProjects
(
rawProjects
)
{
this
.
state
.
frequentProjects
=
rawProjects
;
}
getFrequentProjects
()
{
return
this
.
state
.
frequentProjects
;
}
setSearchedProjects
(
rawProjects
)
{
this
.
state
.
searchedProjects
=
rawProjects
.
map
(
rawProject
=>
({
id
:
rawProject
.
id
,
name
:
rawProject
.
name
,
namespace
:
rawProject
.
name_with_namespace
,
webUrl
:
rawProject
.
web_url
,
avatarUrl
:
rawProject
.
avatar_url
,
}));
}
getSearchedProjects
()
{
return
this
.
state
.
searchedProjects
;
}
clearSearchedProjects
()
{
this
.
state
.
searchedProjects
=
[];
}
}
app/assets/javascripts/vue_shared/components/identicon.vue
View file @
6e4949db
...
...
@@ -9,6 +9,11 @@ export default {
type
:
String
,
required
:
true
,
},
sizeClass
:
{
type
:
String
,
required
:
false
,
default
:
's40'
,
},
},
computed
:
{
/**
...
...
@@ -38,7 +43,8 @@ export default {
<
template
>
<div
class=
"avatar s40 identicon"
class=
"avatar identicon"
:class=
"sizeClass"
:style=
"identiconStyles"
>
{{
identiconTitle
}}
</div>
...
...
app/assets/stylesheets/framework/common.scss
View file @
6e4949db
...
...
@@ -21,6 +21,7 @@
.append-right-default
{
margin-right
:
$gl-padding
;
}
.append-right-20
{
margin-right
:
20px
;
}
.append-bottom-0
{
margin-bottom
:
0
;
}
.append-bottom-5
{
margin-bottom
:
5px
;
}
.append-bottom-10
{
margin-bottom
:
10px
;
}
.append-bottom-15
{
margin-bottom
:
15px
;
}
.append-bottom-20
{
margin-bottom
:
20px
;
}
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
6e4949db
...
...
@@ -829,3 +829,152 @@
}
@include
new-style-dropdown
(
'.js-namespace-select + '
);
header
.navbar-gitlab-new
.header-content
.dropdown-menu.projects-dropdown-menu
{
padding
:
0
;
@media
(
max-width
:
$screen-xs-max
)
{
display
:
table
;
left
:
-50px
;
min-width
:
300px
;
}
}
.projects-dropdown-container
{
display
:
flex
;
flex-direction
:
row
;
width
:
500px
;
height
:
334px
;
.project-dropdown-sidebar
,
.project-dropdown-content
{
padding
:
8px
0
;
}
.loading-animation
{
color
:
$almost-black
;
}
.project-dropdown-sidebar
{
width
:
30%
;
border-right
:
1px
solid
$border-color
;
}
.project-dropdown-content
{
position
:
relative
;
width
:
70%
;
}
@media
(
max-width
:
$screen-xs-max
)
{
flex-direction
:
column
;
width
:
100%
;
height
:
auto
;
flex
:
1
;
.project-dropdown-sidebar
,
.project-dropdown-content
{
width
:
100%
;
}
.project-dropdown-sidebar
{
border-bottom
:
1px
solid
$border-color
;
border-right
:
0
;
}
}
}
.projects-dropdown-container
{
.projects-list-frequent-container
,
.projects-list-search-container
,
{
padding
:
8px
0
;
overflow-y
:
auto
;
}
.section-header
,
.projects-list-frequent-container
li
.section-empty
,
.projects-list-search-container
li
.section-empty
{
padding
:
0
15px
;
}
.section-header
,
.projects-list-frequent-container
li
.section-empty
,
.projects-list-search-container
li
.section-empty
{
color
:
$gl-text-color-secondary
;
font-size
:
$gl-font-size
;
}
.projects-list-frequent-container
,
.projects-list-search-container
{
li
.section-empty.section-failure
{
color
:
$callout-danger-color
;
}
}
.search-input-container
{
position
:
relative
;
padding
:
4px
$gl-padding
;
.search-icon
{
position
:
absolute
;
top
:
13px
;
right
:
25px
;
color
:
$md-area-border
;
}
}
.section-header
{
font-weight
:
700
;
margin-top
:
8px
;
}
.projects-list-search-container
{
height
:
284px
;
}
@media
(
max-width
:
$screen-xs-max
)
{
.projects-list-frequent-container
{
width
:
auto
;
height
:
auto
;
padding-bottom
:
0
;
}
}
}
.projects-list-item-container
{
.project-item-avatar-container
.project-item-metadata-container
{
float
:
left
;
}
.project-title
,
.project-namespace
{
max-width
:
250px
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
&
:hover
{
.project-item-avatar-container
.avatar
{
border-color
:
$md-area-border
;
}
}
.project-title
{
font-size
:
$gl-font-size
;
font-weight
:
400
;
line-height
:
16px
;
}
.project-namespace
{
margin-top
:
4px
;
font-size
:
12px
;
line-height
:
12px
;
color
:
$gl-text-color-secondary
;
}
@media
(
max-width
:
$screen-xs-max
)
{
.project-item-metadata-container
{
float
:
none
;
}
}
}
app/views/layouts/nav/_new_dashboard.html.haml
View file @
6e4949db
%ul
.list-unstyled.navbar-sub-nav
=
nav_link
(
path:
[
'root#index'
,
'projects#trending'
,
'projects#starred'
,
'dashboard/projects#index'
],
html_options:
{
class:
"home"
})
do
=
link_to
dashboard_projects_path
,
title:
'Projects'
,
class:
'dashboard-shortcuts-projects'
do
=
nav_link
(
path:
[
'root#index'
,
'projects#trending'
,
'projects#starred'
,
'dashboard/projects#index'
],
html_options:
{
id:
'nav-projects-dropdown'
,
class:
"home dropdown"
})
do
%a
{
href:
'#'
,
title:
'Projects'
,
data:
{
toggle:
'dropdown'
}
}
Projects
=
icon
(
"chevron-down"
,
class:
"dropdown-chevron"
)
.dropdown-menu.projects-dropdown-menu
=
render
"layouts/nav/projects_dropdown/show"
=
nav_link
(
controller:
[
'dashboard/groups'
,
'explore/groups'
])
do
=
link_to
dashboard_groups_path
,
class:
'dashboard-shortcuts-groups'
,
title:
'Groups'
do
...
...
@@ -31,3 +34,8 @@
%li
.divider
%li
=
link_to
"Help"
,
help_path
,
title:
'About GitLab CE'
-# Shortcut to Dashboard > Projects
%li
.hidden
=
link_to
dashboard_projects_path
,
title:
'Projects'
,
class:
'dashboard-shortcuts-projects'
do
Projects
app/views/layouts/nav/projects_dropdown/_show.html.haml
0 → 100644
View file @
6e4949db
-
project_meta
=
{
id:
@project
.
id
,
name:
@project
.
name
,
namespace:
@project
.
name_with_namespace
,
web_url:
@project
.
web_url
,
avatar_url:
@project
.
avatar_url
}
if
@project
&
.
persisted?
.projects-dropdown-container
.project-dropdown-sidebar
%ul
=
nav_link
(
path:
'dashboard/projects#index'
)
do
=
link_to
dashboard_projects_path
do
=
_
(
'Your projects'
)
=
nav_link
(
path:
'projects#starred'
)
do
=
link_to
starred_dashboard_projects_path
do
=
_
(
'Starred projects'
)
=
nav_link
(
path:
'projects#trending'
)
do
=
link_to
explore_root_path
do
=
_
(
'Explore projects'
)
.project-dropdown-content
#js-projects-dropdown
{
data:
{
user_name:
current_user
.
username
,
project:
project_meta
}
}
changelogs/unreleased/35010-projects-nav-dropdown.yml
0 → 100644
View file @
6e4949db
---
title
:
Add dropdown to Projects nav item
merge_request
:
13866
author
:
type
:
added
config/webpack.config.js
View file @
6e4949db
...
...
@@ -30,7 +30,7 @@ var config = {
blob
:
'./blob_edit/blob_bundle.js'
,
boards
:
'./boards/boards_bundle.js'
,
common
:
'./commons/index.js'
,
common_vue
:
[
'vue'
,
'./vue_shared/common_vue.js'
]
,
common_vue
:
'./vue_shared/vue_resource_interceptor.js'
,
common_d3
:
[
'd3'
],
cycle_analytics
:
'./cycle_analytics/cycle_analytics_bundle.js'
,
commit_pipelines
:
'./commit/pipelines/pipelines_bundle.js'
,
...
...
locale/gitlab.pot
View file @
6e4949db
...
...
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-08-
24 09:29+020
0\n"
"PO-Revision-Date: 2017-08-
24 09:29+020
0\n"
"POT-Creation-Date: 2017-08-
31 17:34+053
0\n"
"PO-Revision-Date: 2017-08-
31 17:34+053
0\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
...
...
@@ -427,6 +427,9 @@ msgstr ""
msgid "Every week (Sundays at 4:00am)"
msgstr ""
msgid "Explore projects"
msgstr ""
msgid "Failed to change the owner"
msgstr ""
...
...
@@ -837,6 +840,27 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
msgid "ProjectsDropdown|Frequently visited"
msgstr ""
msgid "ProjectsDropdown|Loading projects"
msgstr ""
msgid "ProjectsDropdown|No projects matched your query"
msgstr ""
msgid "ProjectsDropdown|Projects you visit often will appear here"
msgstr ""
msgid "ProjectsDropdown|Search projects"
msgstr ""
msgid "ProjectsDropdown|Something went wrong on our end."
msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
msgid "Push events"
msgstr ""
...
...
@@ -950,6 +974,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr ""
msgid "Starred projects"
msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
...
...
@@ -1271,6 +1298,9 @@ msgstr ""
msgid "Your name"
msgstr ""
msgid "Your projects"
msgstr ""
msgid "day"
msgid_plural "days"
msgstr[0] ""
...
...
spec/javascripts/api_spec.js
View file @
6e4949db
...
...
@@ -101,12 +101,13 @@ describe('Api', () => {
it
(
'fetches projects with membership when logged in'
,
(
done
)
=>
{
const
query
=
'dummy query'
;
const
options
=
{
unused
:
'option'
};
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects.json
?simple=true
`
;
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects.json`
;
window
.
gon
.
current_user_id
=
1
;
const
expectedData
=
Object
.
assign
({
search
:
query
,
per_page
:
20
,
membership
:
true
,
simple
:
true
,
},
options
);
spyOn
(
jQuery
,
'ajax'
).
and
.
callFake
((
request
)
=>
{
expect
(
request
.
url
).
toEqual
(
expectedUrl
);
...
...
@@ -124,10 +125,11 @@ describe('Api', () => {
it
(
'fetches projects without membership when not logged in'
,
(
done
)
=>
{
const
query
=
'dummy query'
;
const
options
=
{
unused
:
'option'
};
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects.json
?simple=true
`
;
const
expectedUrl
=
`
${
dummyUrlRoot
}
/api/
${
dummyApiVersion
}
/projects.json`
;
const
expectedData
=
Object
.
assign
({
search
:
query
,
per_page
:
20
,
simple
:
true
,
},
options
);
spyOn
(
jQuery
,
'ajax'
).
and
.
callFake
((
request
)
=>
{
expect
(
request
.
url
).
toEqual
(
expectedUrl
);
...
...
spec/javascripts/project_title_spec.js
View file @
6e4949db
...
...
@@ -41,12 +41,13 @@ describe('Project Title', () => {
window
.
gon
.
current_user_id
=
1
;
$
(
'.js-projects-dropdown-toggle'
).
click
();
expect
(
$menu
).
toHaveClass
(
'open'
);
expect
(
reqUrl
).
toBe
(
`/api/
${
dummyApiVersion
}
/projects.json
?simple=true
`
);
expect
(
reqUrl
).
toBe
(
`/api/
${
dummyApiVersion
}
/projects.json`
);
expect
(
reqData
).
toEqual
({
search
:
''
,
order_by
:
'last_activity_at'
,
per_page
:
20
,
membership
:
true
,
simple
:
true
,
});
$menu
.
find
(
'.dropdown-menu-close-icon'
).
click
();
expect
(
$menu
).
not
.
toHaveClass
(
'open'
);
...
...
spec/javascripts/projects_dropdown/components/app_spec.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
bp
from
'~/breakpoints'
;
import
appComponent
from
'~/projects_dropdown/components/app.vue'
;
import
eventHub
from
'~/projects_dropdown/event_hub'
;
import
ProjectsStore
from
'~/projects_dropdown/store/projects_store'
;
import
ProjectsService
from
'~/projects_dropdown/service/projects_service'
;
import
mountComponent
from
'../../helpers/vue_mount_component_helper'
;
import
{
currentSession
,
mockProject
,
mockRawProject
}
from
'../mock_data'
;
const
createComponent
=
()
=>
{
gon
.
api_version
=
currentSession
.
apiVersion
;
const
Component
=
Vue
.
extend
(
appComponent
);
const
store
=
new
ProjectsStore
();
const
service
=
new
ProjectsService
(
currentSession
.
username
);
return
mountComponent
(
Component
,
{
store
,
service
,
currentUserName
:
currentSession
.
username
,
currentProject
:
currentSession
.
project
,
});
};
const
returnServicePromise
=
(
data
,
failed
)
=>
new
Promise
((
resolve
,
reject
)
=>
{
if
(
failed
)
{
reject
(
data
);
}
else
{
resolve
({
json
()
{
return
data
;
},
});
}
});
describe
(
'AppComponent'
,
()
=>
{
describe
(
'computed'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'frequentProjects'
,
()
=>
{
it
(
'should return list of frequently accessed projects from store'
,
()
=>
{
expect
(
vm
.
frequentProjects
).
toBeDefined
();
expect
(
vm
.
frequentProjects
.
length
).
toBe
(
0
);
vm
.
store
.
setFrequentProjects
([
mockProject
]);
expect
(
vm
.
frequentProjects
).
toBeDefined
();
expect
(
vm
.
frequentProjects
.
length
).
toBe
(
1
);
});
});
describe
(
'searchProjects'
,
()
=>
{
it
(
'should return list of frequently accessed projects from store'
,
()
=>
{
expect
(
vm
.
searchProjects
).
toBeDefined
();
expect
(
vm
.
searchProjects
.
length
).
toBe
(
0
);
vm
.
store
.
setSearchedProjects
([
mockRawProject
]);
expect
(
vm
.
searchProjects
).
toBeDefined
();
expect
(
vm
.
searchProjects
.
length
).
toBe
(
1
);
});
});
});
describe
(
'methods'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'toggleFrequentProjectsList'
,
()
=>
{
it
(
'should toggle props which control visibility of Frequent Projects list from state passed'
,
()
=>
{
vm
.
toggleFrequentProjectsList
(
true
);
expect
(
vm
.
isLoadingProjects
).
toBeFalsy
();
expect
(
vm
.
isSearchListVisible
).
toBeFalsy
();
expect
(
vm
.
isFrequentsListVisible
).
toBeTruthy
();
vm
.
toggleFrequentProjectsList
(
false
);
expect
(
vm
.
isLoadingProjects
).
toBeTruthy
();
expect
(
vm
.
isSearchListVisible
).
toBeTruthy
();
expect
(
vm
.
isFrequentsListVisible
).
toBeFalsy
();
});
});
describe
(
'toggleSearchProjectsList'
,
()
=>
{
it
(
'should toggle props which control visibility of Searched Projects list from state passed'
,
()
=>
{
vm
.
toggleSearchProjectsList
(
true
);
expect
(
vm
.
isLoadingProjects
).
toBeFalsy
();
expect
(
vm
.
isFrequentsListVisible
).
toBeFalsy
();
expect
(
vm
.
isSearchListVisible
).
toBeTruthy
();
vm
.
toggleSearchProjectsList
(
false
);
expect
(
vm
.
isLoadingProjects
).
toBeTruthy
();
expect
(
vm
.
isFrequentsListVisible
).
toBeTruthy
();
expect
(
vm
.
isSearchListVisible
).
toBeFalsy
();
});
});
describe
(
'toggleLoader'
,
()
=>
{
it
(
'should toggle props which control visibility of list loading animation from state passed'
,
()
=>
{
vm
.
toggleLoader
(
true
);
expect
(
vm
.
isFrequentsListVisible
).
toBeFalsy
();
expect
(
vm
.
isSearchListVisible
).
toBeFalsy
();
expect
(
vm
.
isLoadingProjects
).
toBeTruthy
();
vm
.
toggleLoader
(
false
);
expect
(
vm
.
isFrequentsListVisible
).
toBeTruthy
();
expect
(
vm
.
isSearchListVisible
).
toBeTruthy
();
expect
(
vm
.
isLoadingProjects
).
toBeFalsy
();
});
});
describe
(
'fetchFrequentProjects'
,
()
=>
{
it
(
'should set props for loading animation to `true` while frequent projects list is being loaded'
,
()
=>
{
spyOn
(
vm
,
'toggleLoader'
);
vm
.
fetchFrequentProjects
();
expect
(
vm
.
isLocalStorageFailed
).
toBeFalsy
();
expect
(
vm
.
toggleLoader
).
toHaveBeenCalledWith
(
true
);
});
it
(
'should set props for loading animation to `false` and props for frequent projects list to `true` once data is loaded'
,
()
=>
{
const
mockData
=
[
mockProject
];
spyOn
(
vm
.
service
,
'getFrequentProjects'
).
and
.
returnValue
(
mockData
);
spyOn
(
vm
.
store
,
'setFrequentProjects'
);
spyOn
(
vm
,
'toggleFrequentProjectsList'
);
vm
.
fetchFrequentProjects
();
expect
(
vm
.
service
.
getFrequentProjects
).
toHaveBeenCalled
();
expect
(
vm
.
store
.
setFrequentProjects
).
toHaveBeenCalledWith
(
mockData
);
expect
(
vm
.
toggleFrequentProjectsList
).
toHaveBeenCalledWith
(
true
);
});
it
(
'should set props for failure message to `true` when method fails to fetch frequent projects list'
,
()
=>
{
spyOn
(
vm
.
service
,
'getFrequentProjects'
).
and
.
returnValue
(
null
);
spyOn
(
vm
.
store
,
'setFrequentProjects'
);
spyOn
(
vm
,
'toggleFrequentProjectsList'
);
expect
(
vm
.
isLocalStorageFailed
).
toBeFalsy
();
vm
.
fetchFrequentProjects
();
expect
(
vm
.
service
.
getFrequentProjects
).
toHaveBeenCalled
();
expect
(
vm
.
store
.
setFrequentProjects
).
toHaveBeenCalledWith
([]);
expect
(
vm
.
toggleFrequentProjectsList
).
toHaveBeenCalledWith
(
true
);
expect
(
vm
.
isLocalStorageFailed
).
toBeTruthy
();
});
it
(
'should set props for search results list to `true` if search query was already made previously'
,
()
=>
{
spyOn
(
bp
,
'getBreakpointSize'
).
and
.
returnValue
(
'md'
);
spyOn
(
vm
.
service
,
'getFrequentProjects'
);
spyOn
(
vm
,
'toggleSearchProjectsList'
);
vm
.
searchQuery
=
'test'
;
vm
.
fetchFrequentProjects
();
expect
(
vm
.
service
.
getFrequentProjects
).
not
.
toHaveBeenCalled
();
expect
(
vm
.
toggleSearchProjectsList
).
toHaveBeenCalledWith
(
true
);
});
it
(
'should set props for frequent projects list to `true` if search query was already made but screen size is less than 768px'
,
()
=>
{
spyOn
(
bp
,
'getBreakpointSize'
).
and
.
returnValue
(
'sm'
);
spyOn
(
vm
,
'toggleSearchProjectsList'
);
spyOn
(
vm
.
service
,
'getFrequentProjects'
);
vm
.
searchQuery
=
'test'
;
vm
.
fetchFrequentProjects
();
expect
(
vm
.
service
.
getFrequentProjects
).
toHaveBeenCalled
();
expect
(
vm
.
toggleSearchProjectsList
).
not
.
toHaveBeenCalled
();
});
});
describe
(
'fetchSearchedProjects'
,
()
=>
{
const
searchQuery
=
'test'
;
it
(
'should perform search with provided search query'
,
(
done
)
=>
{
const
mockData
=
[
mockRawProject
];
spyOn
(
vm
,
'toggleLoader'
);
spyOn
(
vm
,
'toggleSearchProjectsList'
);
spyOn
(
vm
.
service
,
'getSearchedProjects'
).
and
.
returnValue
(
returnServicePromise
(
mockData
));
spyOn
(
vm
.
store
,
'setSearchedProjects'
);
vm
.
fetchSearchedProjects
(
searchQuery
);
setTimeout
(()
=>
{
expect
(
vm
.
searchQuery
).
toBe
(
searchQuery
);
expect
(
vm
.
toggleLoader
).
toHaveBeenCalledWith
(
true
);
expect
(
vm
.
service
.
getSearchedProjects
).
toHaveBeenCalledWith
(
searchQuery
);
expect
(
vm
.
toggleSearchProjectsList
).
toHaveBeenCalledWith
(
true
);
expect
(
vm
.
store
.
setSearchedProjects
).
toHaveBeenCalledWith
(
mockData
);
done
();
},
0
);
});
it
(
'should update props for showing search failure'
,
(
done
)
=>
{
spyOn
(
vm
,
'toggleSearchProjectsList'
);
spyOn
(
vm
.
service
,
'getSearchedProjects'
).
and
.
returnValue
(
returnServicePromise
({},
true
));
vm
.
fetchSearchedProjects
(
searchQuery
);
setTimeout
(()
=>
{
expect
(
vm
.
searchQuery
).
toBe
(
searchQuery
);
expect
(
vm
.
service
.
getSearchedProjects
).
toHaveBeenCalledWith
(
searchQuery
);
expect
(
vm
.
isSearchFailed
).
toBeTruthy
();
expect
(
vm
.
toggleSearchProjectsList
).
toHaveBeenCalledWith
(
true
);
done
();
},
0
);
});
});
describe
(
'logCurrentProjectAccess'
,
()
=>
{
it
(
'should log current project access via service'
,
(
done
)
=>
{
spyOn
(
vm
.
service
,
'logProjectAccess'
);
vm
.
currentProject
=
mockProject
;
vm
.
logCurrentProjectAccess
();
setTimeout
(()
=>
{
expect
(
vm
.
service
.
logProjectAccess
).
toHaveBeenCalledWith
(
mockProject
);
done
();
},
1
);
});
});
describe
(
'handleSearchClear'
,
()
=>
{
it
(
'should show frequent projects list when search input is cleared'
,
()
=>
{
spyOn
(
vm
.
store
,
'clearSearchedProjects'
);
spyOn
(
vm
,
'toggleFrequentProjectsList'
);
vm
.
handleSearchClear
();
expect
(
vm
.
toggleFrequentProjectsList
).
toHaveBeenCalledWith
(
true
);
expect
(
vm
.
store
.
clearSearchedProjects
).
toHaveBeenCalled
();
expect
(
vm
.
searchQuery
).
toBe
(
''
);
});
});
describe
(
'handleSearchFailure'
,
()
=>
{
it
(
'should show failure message within dropdown'
,
()
=>
{
spyOn
(
vm
,
'toggleSearchProjectsList'
);
vm
.
handleSearchFailure
();
expect
(
vm
.
toggleSearchProjectsList
).
toHaveBeenCalledWith
(
true
);
expect
(
vm
.
isSearchFailed
).
toBeTruthy
();
});
});
});
describe
(
'created'
,
()
=>
{
it
(
'should bind event listeners on eventHub'
,
(
done
)
=>
{
spyOn
(
eventHub
,
'$on'
);
createComponent
().
$mount
();
Vue
.
nextTick
(()
=>
{
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'dropdownOpen'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'searchProjects'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'searchCleared'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'searchFailed'
,
jasmine
.
any
(
Function
));
done
();
});
});
});
describe
(
'beforeDestroy'
,
()
=>
{
it
(
'should unbind event listeners on eventHub'
,
(
done
)
=>
{
const
vm
=
createComponent
();
spyOn
(
eventHub
,
'$off'
);
vm
.
$mount
();
vm
.
$destroy
();
Vue
.
nextTick
(()
=>
{
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'dropdownOpen'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'searchProjects'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'searchCleared'
,
jasmine
.
any
(
Function
));
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'searchFailed'
,
jasmine
.
any
(
Function
));
done
();
});
});
});
describe
(
'template'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'should render search input'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'.search-input-container'
)).
toBeDefined
();
});
it
(
'should render loading animation'
,
(
done
)
=>
{
vm
.
toggleLoader
(
true
);
Vue
.
nextTick
(()
=>
{
const
loadingEl
=
vm
.
$el
.
querySelector
(
'.loading-animation'
);
expect
(
loadingEl
).
toBeDefined
();
expect
(
loadingEl
.
classList
.
contains
(
'prepend-top-20'
)).
toBeTruthy
();
expect
(
loadingEl
.
querySelector
(
'i'
).
getAttribute
(
'aria-label'
)).
toBe
(
'Loading projects'
);
done
();
});
});
it
(
'should render frequent projects list header'
,
(
done
)
=>
{
vm
.
toggleFrequentProjectsList
(
true
);
Vue
.
nextTick
(()
=>
{
const
sectionHeaderEl
=
vm
.
$el
.
querySelector
(
'.section-header'
);
expect
(
sectionHeaderEl
).
toBeDefined
();
expect
(
sectionHeaderEl
.
innerText
.
trim
()).
toBe
(
'Frequently visited'
);
done
();
});
});
it
(
'should render frequent projects list'
,
(
done
)
=>
{
vm
.
toggleFrequentProjectsList
(
true
);
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'.projects-list-frequent-container'
)).
toBeDefined
();
done
();
});
});
it
(
'should render searched projects list'
,
(
done
)
=>
{
vm
.
toggleSearchProjectsList
(
true
);
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'.section-header'
)).
toBe
(
null
);
expect
(
vm
.
$el
.
querySelector
(
'.projects-list-search-container'
)).
toBeDefined
();
done
();
});
});
});
});
spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
projectsListFrequentComponent
from
'~/projects_dropdown/components/projects_list_frequent.vue'
;
import
mountComponent
from
'../../helpers/vue_mount_component_helper'
;
import
{
mockFrequents
}
from
'../mock_data'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
projectsListFrequentComponent
);
return
mountComponent
(
Component
,
{
projects
:
mockFrequents
,
localStorageFailed
:
false
,
});
};
describe
(
'ProjectsListFrequentComponent'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'computed'
,
()
=>
{
describe
(
'isListEmpty'
,
()
=>
{
it
(
'should return `true` or `false` representing whether if `projects` is empty of not'
,
()
=>
{
vm
.
projects
=
[];
expect
(
vm
.
isListEmpty
).
toBeTruthy
();
vm
.
projects
=
mockFrequents
;
expect
(
vm
.
isListEmpty
).
toBeFalsy
();
});
});
describe
(
'listEmptyMessage'
,
()
=>
{
it
(
'should return appropriate empty list message based on value of `localStorageFailed` prop'
,
()
=>
{
vm
.
localStorageFailed
=
true
;
expect
(
vm
.
listEmptyMessage
).
toBe
(
'This feature requires browser localStorage support'
);
vm
.
localStorageFailed
=
false
;
expect
(
vm
.
listEmptyMessage
).
toBe
(
'Projects you visit often will appear here'
);
});
});
});
describe
(
'template'
,
()
=>
{
it
(
'should render component element with list of projects'
,
(
done
)
=>
{
vm
.
projects
=
mockFrequents
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'projects-list-frequent-container'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
querySelectorAll
(
'ul.list-unstyled'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'li.projects-list-item-container'
).
length
).
toBe
(
5
);
done
();
});
});
it
(
'should render component element with empty message'
,
(
done
)
=>
{
vm
.
projects
=
[];
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'li.section-empty'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'li.projects-list-item-container'
).
length
).
toBe
(
0
);
done
();
});
});
});
});
spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
projectsListItemComponent
from
'~/projects_dropdown/components/projects_list_item.vue'
;
import
mountComponent
from
'../../helpers/vue_mount_component_helper'
;
import
{
mockProject
}
from
'../mock_data'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
projectsListItemComponent
);
return
mountComponent
(
Component
,
{
projectId
:
mockProject
.
id
,
projectName
:
mockProject
.
name
,
namespace
:
mockProject
.
namespace
,
webUrl
:
mockProject
.
webUrl
,
avatarUrl
:
mockProject
.
avatarUrl
,
});
};
describe
(
'ProjectsListItemComponent'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'computed'
,
()
=>
{
describe
(
'hasAvatar'
,
()
=>
{
it
(
'should return `true` or `false` if whether avatar is present or not'
,
()
=>
{
vm
.
avatarUrl
=
'path/to/avatar.png'
;
expect
(
vm
.
hasAvatar
).
toBeTruthy
();
vm
.
avatarUrl
=
null
;
expect
(
vm
.
hasAvatar
).
toBeFalsy
();
});
});
describe
(
'highlightedProjectName'
,
()
=>
{
it
(
'should enclose part of project name in <b> & </b> which matches with `matcher` prop'
,
()
=>
{
vm
.
matcher
=
'lab'
;
expect
(
vm
.
highlightedProjectName
).
toContain
(
'<b>Lab</b>'
);
});
it
(
'should return project name as it is if `matcher` is not available'
,
()
=>
{
vm
.
matcher
=
null
;
expect
(
vm
.
highlightedProjectName
).
toBe
(
mockProject
.
name
);
});
});
});
describe
(
'template'
,
()
=>
{
it
(
'should render component element'
,
()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'projects-list-item-container'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
querySelectorAll
(
'a'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'.project-item-avatar-container'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'.project-item-metadata-container'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'.project-title'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'.project-namespace'
).
length
).
toBe
(
1
);
});
});
});
spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
projectsListSearchComponent
from
'~/projects_dropdown/components/projects_list_search.vue'
;
import
mountComponent
from
'../../helpers/vue_mount_component_helper'
;
import
{
mockProject
}
from
'../mock_data'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
projectsListSearchComponent
);
return
mountComponent
(
Component
,
{
projects
:
[
mockProject
],
matcher
:
'lab'
,
searchFailed
:
false
,
});
};
describe
(
'ProjectsListSearchComponent'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'computed'
,
()
=>
{
describe
(
'isListEmpty'
,
()
=>
{
it
(
'should return `true` or `false` representing whether if `projects` is empty of not'
,
()
=>
{
vm
.
projects
=
[];
expect
(
vm
.
isListEmpty
).
toBeTruthy
();
vm
.
projects
=
[
mockProject
];
expect
(
vm
.
isListEmpty
).
toBeFalsy
();
});
});
describe
(
'listEmptyMessage'
,
()
=>
{
it
(
'should return appropriate empty list message based on value of `searchFailed` prop'
,
()
=>
{
vm
.
searchFailed
=
true
;
expect
(
vm
.
listEmptyMessage
).
toBe
(
'Something went wrong on our end.'
);
vm
.
searchFailed
=
false
;
expect
(
vm
.
listEmptyMessage
).
toBe
(
'No projects matched your query'
);
});
});
});
describe
(
'template'
,
()
=>
{
it
(
'should render component element with list of projects'
,
(
done
)
=>
{
vm
.
projects
=
[
mockProject
];
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
classList
.
contains
(
'projects-list-search-container'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
querySelectorAll
(
'ul.list-unstyled'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'li.projects-list-item-container'
).
length
).
toBe
(
1
);
done
();
});
});
it
(
'should render component element with empty message'
,
(
done
)
=>
{
vm
.
projects
=
[];
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'li.section-empty'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'li.projects-list-item-container'
).
length
).
toBe
(
0
);
done
();
});
});
it
(
'should render component element with failure message'
,
(
done
)
=>
{
vm
.
searchFailed
=
true
;
vm
.
projects
=
[];
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelectorAll
(
'li.section-empty.section-failure'
).
length
).
toBe
(
1
);
expect
(
vm
.
$el
.
querySelectorAll
(
'li.projects-list-item-container'
).
length
).
toBe
(
0
);
done
();
});
});
});
});
spec/javascripts/projects_dropdown/components/search_spec.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
searchComponent
from
'~/projects_dropdown/components/search.vue'
;
import
eventHub
from
'~/projects_dropdown/event_hub'
;
import
mountComponent
from
'../../helpers/vue_mount_component_helper'
;
const
createComponent
=
()
=>
{
const
Component
=
Vue
.
extend
(
searchComponent
);
return
mountComponent
(
Component
);
};
describe
(
'SearchComponent'
,
()
=>
{
describe
(
'methods'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'setFocus'
,
()
=>
{
it
(
'should set focus to search input'
,
()
=>
{
spyOn
(
vm
.
$refs
.
search
,
'focus'
);
vm
.
setFocus
();
expect
(
vm
.
$refs
.
search
.
focus
).
toHaveBeenCalled
();
});
});
describe
(
'emitSearchEvents'
,
()
=>
{
it
(
'should emit `searchProjects` event via eventHub when `searchQuery` present'
,
()
=>
{
const
searchQuery
=
'test'
;
spyOn
(
eventHub
,
'$emit'
);
vm
.
searchQuery
=
searchQuery
;
vm
.
emitSearchEvents
();
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'searchProjects'
,
searchQuery
);
});
it
(
'should emit `searchCleared` event via eventHub when `searchQuery` is cleared'
,
()
=>
{
spyOn
(
eventHub
,
'$emit'
);
vm
.
searchQuery
=
''
;
vm
.
emitSearchEvents
();
expect
(
eventHub
.
$emit
).
toHaveBeenCalledWith
(
'searchCleared'
);
});
});
});
describe
(
'mounted'
,
()
=>
{
it
(
'should listen `dropdownOpen` event'
,
(
done
)
=>
{
spyOn
(
eventHub
,
'$on'
);
createComponent
();
Vue
.
nextTick
(()
=>
{
expect
(
eventHub
.
$on
).
toHaveBeenCalledWith
(
'dropdownOpen'
,
jasmine
.
any
(
Function
));
done
();
});
});
});
describe
(
'beforeDestroy'
,
()
=>
{
it
(
'should unbind event listeners on eventHub'
,
(
done
)
=>
{
const
vm
=
createComponent
();
spyOn
(
eventHub
,
'$off'
);
vm
.
$mount
();
vm
.
$destroy
();
Vue
.
nextTick
(()
=>
{
expect
(
eventHub
.
$off
).
toHaveBeenCalledWith
(
'dropdownOpen'
,
jasmine
.
any
(
Function
));
done
();
});
});
});
describe
(
'template'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
afterEach
(()
=>
{
vm
.
$destroy
();
});
it
(
'should render component element'
,
()
=>
{
const
inputEl
=
vm
.
$el
.
querySelector
(
'input.form-control'
);
expect
(
vm
.
$el
.
classList
.
contains
(
'search-input-container'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
classList
.
contains
(
'hidden-xs'
)).
toBeTruthy
();
expect
(
inputEl
).
not
.
toBe
(
null
);
expect
(
inputEl
.
getAttribute
(
'placeholder'
)).
toBe
(
'Search projects'
);
expect
(
vm
.
$el
.
querySelector
(
'.search-icon'
)).
toBeDefined
();
});
});
});
spec/javascripts/projects_dropdown/mock_data.js
0 → 100644
View file @
6e4949db
export
const
currentSession
=
{
username
:
'root'
,
storageKey
:
'root/frequent-projects'
,
apiVersion
:
'v4'
,
project
:
{
id
:
1
,
name
:
'dummy-project'
,
namespace
:
'SamepleGroup / Dummy-Project'
,
webUrl
:
'http://127.0.0.1/samplegroup/dummy-project'
,
avatarUrl
:
null
,
lastAccessedOn
:
Date
.
now
(),
},
};
export
const
mockProject
=
{
id
:
1
,
name
:
'GitLab Community Edition'
,
namespace
:
'gitlab-org / gitlab-ce'
,
webUrl
:
'http://127.0.0.1:3000/gitlab-org/gitlab-ce'
,
avatarUrl
:
null
,
};
export
const
mockRawProject
=
{
id
:
1
,
name
:
'GitLab Community Edition'
,
name_with_namespace
:
'gitlab-org / gitlab-ce'
,
web_url
:
'http://127.0.0.1:3000/gitlab-org/gitlab-ce'
,
avatar_url
:
null
,
};
export
const
mockFrequents
=
[
{
id
:
1
,
name
:
'GitLab Community Edition'
,
namespace
:
'gitlab-org / gitlab-ce'
,
webUrl
:
'http://127.0.0.1:3000/gitlab-org/gitlab-ce'
,
avatarUrl
:
null
,
},
{
id
:
2
,
name
:
'GitLab CI'
,
namespace
:
'gitlab-org / gitlab-ci'
,
webUrl
:
'http://127.0.0.1:3000/gitlab-org/gitlab-ci'
,
avatarUrl
:
null
,
},
{
id
:
3
,
name
:
'Typeahead.Js'
,
namespace
:
'twitter / typeahead-js'
,
webUrl
:
'http://127.0.0.1:3000/twitter/typeahead-js'
,
avatarUrl
:
'/uploads/-/system/project/avatar/7/TWBS.png'
,
},
{
id
:
4
,
name
:
'Intel'
,
namespace
:
'platform / hardware / bsp / intel'
,
webUrl
:
'http://127.0.0.1:3000/platform/hardware/bsp/intel'
,
avatarUrl
:
null
,
},
{
id
:
5
,
name
:
'v4.4'
,
namespace
:
'platform / hardware / bsp / kernel / common / v4.4'
,
webUrl
:
'http://localhost:3000/platform/hardware/bsp/kernel/common/v4.4'
,
avatarUrl
:
null
,
},
];
export
const
unsortedFrequents
=
[
{
id
:
1
,
frequency
:
12
,
lastAccessedOn
:
1491400843391
},
{
id
:
2
,
frequency
:
14
,
lastAccessedOn
:
1488240890738
},
{
id
:
3
,
frequency
:
44
,
lastAccessedOn
:
1497675908472
},
{
id
:
4
,
frequency
:
8
,
lastAccessedOn
:
1497979281815
},
{
id
:
5
,
frequency
:
34
,
lastAccessedOn
:
1488089211943
},
{
id
:
6
,
frequency
:
14
,
lastAccessedOn
:
1493517292488
},
{
id
:
7
,
frequency
:
42
,
lastAccessedOn
:
1486815299875
},
{
id
:
8
,
frequency
:
33
,
lastAccessedOn
:
1500762279114
},
{
id
:
10
,
frequency
:
46
,
lastAccessedOn
:
1483251641543
},
];
/**
* This const has a specific order which tests authenticity
* of `ProjectsService.getTopFrequentProjects` method so
* DO NOT change order of items in this const.
*/
export
const
sortedFrequents
=
[
{
id
:
10
,
frequency
:
46
,
lastAccessedOn
:
1483251641543
},
{
id
:
3
,
frequency
:
44
,
lastAccessedOn
:
1497675908472
},
{
id
:
7
,
frequency
:
42
,
lastAccessedOn
:
1486815299875
},
{
id
:
5
,
frequency
:
34
,
lastAccessedOn
:
1488089211943
},
{
id
:
8
,
frequency
:
33
,
lastAccessedOn
:
1500762279114
},
{
id
:
6
,
frequency
:
14
,
lastAccessedOn
:
1493517292488
},
{
id
:
2
,
frequency
:
14
,
lastAccessedOn
:
1488240890738
},
{
id
:
1
,
frequency
:
12
,
lastAccessedOn
:
1491400843391
},
{
id
:
4
,
frequency
:
8
,
lastAccessedOn
:
1497979281815
},
];
spec/javascripts/projects_dropdown/service/projects_service_spec.js
0 → 100644
View file @
6e4949db
import
Vue
from
'vue'
;
import
VueResource
from
'vue-resource'
;
import
bp
from
'~/breakpoints'
;
import
ProjectsService
from
'~/projects_dropdown/service/projects_service'
;
import
{
FREQUENT_PROJECTS
}
from
'~/projects_dropdown/constants'
;
import
{
currentSession
,
unsortedFrequents
,
sortedFrequents
}
from
'../mock_data'
;
Vue
.
use
(
VueResource
);
FREQUENT_PROJECTS
.
MAX_COUNT
=
3
;
describe
(
'ProjectsService'
,
()
=>
{
let
service
;
beforeEach
(()
=>
{
gon
.
api_version
=
currentSession
.
apiVersion
;
service
=
new
ProjectsService
(
currentSession
.
username
);
});
describe
(
'contructor'
,
()
=>
{
it
(
'should initialize default properties of class'
,
()
=>
{
expect
(
service
.
isLocalStorageAvailable
).
toBeTruthy
();
expect
(
service
.
currentUserName
).
toBe
(
currentSession
.
username
);
expect
(
service
.
storageKey
).
toBe
(
currentSession
.
storageKey
);
expect
(
service
.
projectsPath
).
toBeDefined
();
});
});
describe
(
'getSearchedProjects'
,
()
=>
{
it
(
'should return promise from VueResource HTTP GET'
,
()
=>
{
spyOn
(
service
.
projectsPath
,
'get'
).
and
.
stub
();
const
searchQuery
=
'lab'
;
const
queryParams
=
{
simple
:
false
,
per_page
:
20
,
membership
:
false
,
order_by
:
'last_activity_at'
,
search
:
searchQuery
,
};
service
.
getSearchedProjects
(
searchQuery
);
expect
(
service
.
projectsPath
.
get
).
toHaveBeenCalledWith
(
queryParams
);
});
});
describe
(
'logProjectAccess'
,
()
=>
{
let
storage
;
beforeEach
(()
=>
{
storage
=
{};
spyOn
(
window
.
localStorage
,
'setItem'
).
and
.
callFake
((
storageKey
,
value
)
=>
{
storage
[
storageKey
]
=
value
;
});
spyOn
(
window
.
localStorage
,
'getItem'
).
and
.
callFake
((
storageKey
)
=>
{
if
(
storage
[
storageKey
])
{
return
storage
[
storageKey
];
}
return
null
;
});
});
it
(
'should create a project store if it does not exist and adds a project'
,
()
=>
{
service
.
logProjectAccess
(
currentSession
.
project
);
const
projects
=
JSON
.
parse
(
storage
[
currentSession
.
storageKey
]);
expect
(
projects
.
length
).
toBe
(
1
);
expect
(
projects
[
0
].
frequency
).
toBe
(
1
);
expect
(
projects
[
0
].
lastAccessedOn
).
toBeDefined
();
});
it
(
'should prevent inserting same report multiple times into store'
,
()
=>
{
service
.
logProjectAccess
(
currentSession
.
project
);
service
.
logProjectAccess
(
currentSession
.
project
);
const
projects
=
JSON
.
parse
(
storage
[
currentSession
.
storageKey
]);
expect
(
projects
.
length
).
toBe
(
1
);
});
it
(
'should increase frequency of report if it was logged multiple times over the course of an hour'
,
()
=>
{
let
projects
;
spyOn
(
Math
,
'abs'
).
and
.
returnValue
(
3600001
);
// this will lead to `diff` > 1;
service
.
logProjectAccess
(
currentSession
.
project
);
projects
=
JSON
.
parse
(
storage
[
currentSession
.
storageKey
]);
expect
(
projects
[
0
].
frequency
).
toBe
(
1
);
service
.
logProjectAccess
(
currentSession
.
project
);
projects
=
JSON
.
parse
(
storage
[
currentSession
.
storageKey
]);
expect
(
projects
[
0
].
frequency
).
toBe
(
2
);
expect
(
projects
[
0
].
lastAccessedOn
).
not
.
toBe
(
currentSession
.
project
.
lastAccessedOn
);
});
it
(
'should always update project metadata'
,
()
=>
{
let
projects
;
const
oldProject
=
{
...
currentSession
.
project
,
};
const
newProject
=
{
...
currentSession
.
project
,
name
:
'New Name'
,
avatarUrl
:
'new/avatar.png'
,
namespace
:
'New / Namespace'
,
webUrl
:
'http://localhost/new/web/url'
,
};
service
.
logProjectAccess
(
oldProject
);
projects
=
JSON
.
parse
(
storage
[
currentSession
.
storageKey
]);
expect
(
projects
[
0
].
name
).
toBe
(
oldProject
.
name
);
expect
(
projects
[
0
].
avatarUrl
).
toBe
(
oldProject
.
avatarUrl
);
expect
(
projects
[
0
].
namespace
).
toBe
(
oldProject
.
namespace
);
expect
(
projects
[
0
].
webUrl
).
toBe
(
oldProject
.
webUrl
);
service
.
logProjectAccess
(
newProject
);
projects
=
JSON
.
parse
(
storage
[
currentSession
.
storageKey
]);
expect
(
projects
[
0
].
name
).
toBe
(
newProject
.
name
);
expect
(
projects
[
0
].
avatarUrl
).
toBe
(
newProject
.
avatarUrl
);
expect
(
projects
[
0
].
namespace
).
toBe
(
newProject
.
namespace
);
expect
(
projects
[
0
].
webUrl
).
toBe
(
newProject
.
webUrl
);
});
it
(
'should not add more than 20 projects in store'
,
()
=>
{
for
(
let
i
=
1
;
i
<=
5
;
i
+=
1
)
{
const
project
=
Object
.
assign
(
currentSession
.
project
,
{
id
:
i
});
service
.
logProjectAccess
(
project
);
}
const
projects
=
JSON
.
parse
(
storage
[
currentSession
.
storageKey
]);
expect
(
projects
.
length
).
toBe
(
3
);
});
});
describe
(
'getTopFrequentProjects'
,
()
=>
{
let
storage
=
{};
beforeEach
(()
=>
{
storage
[
currentSession
.
storageKey
]
=
JSON
.
stringify
(
unsortedFrequents
);
spyOn
(
window
.
localStorage
,
'getItem'
).
and
.
callFake
((
storageKey
)
=>
{
if
(
storage
[
storageKey
])
{
return
storage
[
storageKey
];
}
return
null
;
});
});
it
(
'should return top 5 frequently accessed projects for desktop screens'
,
()
=>
{
spyOn
(
bp
,
'getBreakpointSize'
).
and
.
returnValue
(
'md'
);
const
frequentProjects
=
service
.
getTopFrequentProjects
();
expect
(
frequentProjects
.
length
).
toBe
(
5
);
frequentProjects
.
forEach
((
project
,
index
)
=>
{
expect
(
project
.
id
).
toBe
(
sortedFrequents
[
index
].
id
);
});
});
it
(
'should return top 3 frequently accessed projects for mobile screens'
,
()
=>
{
spyOn
(
bp
,
'getBreakpointSize'
).
and
.
returnValue
(
'sm'
);
const
frequentProjects
=
service
.
getTopFrequentProjects
();
expect
(
frequentProjects
.
length
).
toBe
(
3
);
frequentProjects
.
forEach
((
project
,
index
)
=>
{
expect
(
project
.
id
).
toBe
(
sortedFrequents
[
index
].
id
);
});
});
it
(
'should return empty array if there are no projects available in store'
,
()
=>
{
storage
=
{};
expect
(
service
.
getTopFrequentProjects
().
length
).
toBe
(
0
);
});
});
});
spec/javascripts/projects_dropdown/store/projects_store_spec.js
0 → 100644
View file @
6e4949db
import
ProjectsStore
from
'~/projects_dropdown/store/projects_store'
;
import
{
mockProject
,
mockRawProject
}
from
'../mock_data'
;
describe
(
'ProjectsStore'
,
()
=>
{
let
store
;
beforeEach
(()
=>
{
store
=
new
ProjectsStore
();
});
describe
(
'setFrequentProjects'
,
()
=>
{
it
(
'should set frequent projects list to state'
,
()
=>
{
store
.
setFrequentProjects
([
mockProject
]);
expect
(
store
.
getFrequentProjects
().
length
).
toBe
(
1
);
expect
(
store
.
getFrequentProjects
()[
0
].
id
).
toBe
(
mockProject
.
id
);
});
});
describe
(
'setSearchedProjects'
,
()
=>
{
it
(
'should set searched projects list to state'
,
()
=>
{
store
.
setSearchedProjects
([
mockRawProject
]);
const
processedProjects
=
store
.
getSearchedProjects
();
expect
(
processedProjects
.
length
).
toBe
(
1
);
expect
(
processedProjects
[
0
].
id
).
toBe
(
mockRawProject
.
id
);
expect
(
processedProjects
[
0
].
namespace
).
toBe
(
mockRawProject
.
name_with_namespace
);
expect
(
processedProjects
[
0
].
webUrl
).
toBe
(
mockRawProject
.
web_url
);
expect
(
processedProjects
[
0
].
avatarUrl
).
toBe
(
mockRawProject
.
avatar_url
);
});
});
describe
(
'clearSearchedProjects'
,
()
=>
{
it
(
'should clear searched projects list from state'
,
()
=>
{
store
.
setSearchedProjects
([
mockRawProject
]);
expect
(
store
.
getSearchedProjects
().
length
).
toBe
(
1
);
store
.
clearSearchedProjects
();
expect
(
store
.
getSearchedProjects
().
length
).
toBe
(
0
);
});
});
});
spec/javascripts/vue_shared/components/identicon_spec.js
View file @
6e4949db
import
Vue
from
'vue'
;
import
identiconComponent
from
'~/vue_shared/components/identicon.vue'
;
const
createComponent
=
()
=>
{
const
createComponent
=
(
sizeClass
)
=>
{
const
Component
=
Vue
.
extend
(
identiconComponent
);
return
new
Component
({
propsData
:
{
entityId
:
1
,
entityName
:
'entity-name'
,
sizeClass
,
},
}).
$mount
();
};
describe
(
'IdenticonComponent'
,
()
=>
{
describe
(
'computed'
,
()
=>
{
let
vm
;
beforeEach
(()
=>
{
vm
=
createComponent
();
});
describe
(
'computed'
,
()
=>
{
afterEach
(()
=>
{
vm
.
$destroy
();
});
describe
(
'identiconStyles'
,
()
=>
{
it
(
'should return styles attribute value with `background-color` property'
,
()
=>
{
vm
.
entityId
=
4
;
...
...
@@ -48,9 +53,20 @@ describe('IdenticonComponent', () => {
describe
(
'template'
,
()
=>
{
it
(
'should render identicon'
,
()
=>
{
const
vm
=
createComponent
();
expect
(
vm
.
$el
.
nodeName
).
toBe
(
'DIV'
);
expect
(
vm
.
$el
.
classList
.
contains
(
'identicon'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
classList
.
contains
(
's40'
)).
toBeTruthy
();
expect
(
vm
.
$el
.
getAttribute
(
'style'
).
indexOf
(
'background-color'
)
>
-
1
).
toBeTruthy
();
vm
.
$destroy
();
});
it
(
'should render identicon with provided sizing class'
,
()
=>
{
const
vm
=
createComponent
(
's32'
);
expect
(
vm
.
$el
.
classList
.
contains
(
's32'
)).
toBeTruthy
();
vm
.
$destroy
();
});
});
});
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