diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f8d388f..6280b4f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,15 +4,26 @@
## 1.10.0 ??? (unreleased)
### Features
-- Upload attachments on US/issue/task lightbox.
-- Attachments image gallery view mode in detail pages.
-- Drag files from desktop to attachments section.
-- Drag files from desktop in wysiwyg textareas.
- New design for the detail pages slidebar.
-- Sticky project navigation bar.
- Added 'Assign to me' button in User Stories, Tasks and Issues detail pages. (thanks to [@allistera](https://github.com/allistera)).
+- Attachments:
+ - Upload attachments on US/issue/task lightbox.
+ - Attachments image gallery view mode in detail pages.
+ - Drag files from desktop to attachments section.
+ - Drag files from desktop in wysiwyg textareas.
+- Project:
+ - Add a logo to your project.
+ - Denotes that your project is looking for people and add an explanation.
+- Discover section:
+ - List most liked and most active project (last week/month/year or all time).
+ - List featured project.
+ - Search projects:
+ - Full text search with priorities over title, tags and description fields.
+ - Order results alphabeticaly, by most liked or more actived.
+ - Filter by 'use kanban', 'use scrum' or 'looking for people'.
### Misc
+- Sticky project navigation bar.
- Lots of small and not so small bugfixes.
diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee
index 24f955e1..54ff9b5d 100644
--- a/app/coffee/app.coffee
+++ b/app/coffee/app.coffee
@@ -66,9 +66,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
$routeProvider.when("/",
{
templateUrl: "home/home.html",
- access: {
- requiresLogin: true
- },
+ controller: "Home",
+ controllerAs: "vm"
loader: true,
title: "HOME.PAGE_TITLE",
loader: true,
@@ -77,6 +76,27 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
}
)
+ $routeProvider.when("/discover",
+ {
+ templateUrl: "discover/discover-home/discover-home.html",
+ controller: "DiscoverHome",
+ controllerAs: "vm",
+ title: "PROJECT.NAVIGATION.DISCOVER",
+ loader: true
+ }
+ )
+
+ $routeProvider.when("/discover/search",
+ {
+ templateUrl: "discover/discover-search/discover-search.html",
+ title: "PROJECT.NAVIGATION.DISCOVER",
+ loader: true,
+ controller: "DiscoverSearch",
+ controllerAs: "vm",
+ reloadOnSearch: false
+ }
+ )
+
$routeProvider.when("/projects/",
{
templateUrl: "projects/listing/projects-listing.html",
@@ -577,7 +597,7 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na
$rootscope.$evalAsync(cb)
$events.setupConnection()
-
+
# Load user
if $auth.isAuthenticated()
user = $auth.getUser()
@@ -664,6 +684,7 @@ modules = [
"taigaHome",
"taigaUserTimeline",
"taigaExternalApps",
+ "taigaDiscover",
# template cache
"templates",
diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee
index 74055a96..4a6bee65 100644
--- a/app/coffee/modules/admin/project-profile.coffee
+++ b/app/coffee/modules/admin/project-profile.coffee
@@ -449,3 +449,64 @@ CsvIssueDirective = ($translate) ->
}
module.directive("tgCsvIssue", ["$translate", CsvIssueDirective])
+
+
+#############################################################################
+## Project Logo Directive
+#############################################################################
+
+ProjectLogoDirective = ($auth, $model, $rs, $confirm) ->
+ link = ($scope, $el, $attrs) ->
+ showSizeInfo = ->
+ $el.find(".size-info").addClass("active")
+
+ onSuccess = (response) ->
+ project = $model.make_model("projects", response.data)
+ $scope.project = project
+
+ $el.find('.loading-overlay').removeClass('active')
+ $confirm.notify('success')
+
+ onError = (response) ->
+ showSizeInfo() if response.status == 413
+ $el.find('.loading-overlay').removeClass('active')
+ $confirm.notify('error', response.data._error_message)
+
+ # Change photo
+ $el.on "click", ".js-change-logo", ->
+ $el.find("#logo-field").click()
+
+ $el.on "change", "#logo-field", (event) ->
+ if $scope.logoAttachment
+ $el.find('.loading-overlay').addClass("active")
+ $rs.projects.changeLogo($scope.project.id, $scope.logoAttachment).then(onSuccess, onError)
+
+ # Use default photo
+ $el.on "click", "a.js-use-default-logo", (event) ->
+ $el.find('.loading-overlay').addClass("active")
+ $rs.projects.removeLogo($scope.project.id).then(onSuccess, onError)
+
+ $scope.$on "$destroy", ->
+ $el.off()
+
+ return {link:link}
+
+module.directive("tgProjectLogo", ["$tgAuth", "$tgModel", "$tgResources", "$tgConfirm", ProjectLogoDirective])
+
+
+#############################################################################
+## Project Logo Model Directive
+#############################################################################
+
+ProjectLogoModelDirective = ($parse) ->
+ link = ($scope, $el, $attrs) ->
+ model = $parse($attrs.tgProjectLogoModel)
+ modelSetter = model.assign
+
+ $el.bind 'change', ->
+ $scope.$apply ->
+ modelSetter($scope, $el[0].files[0])
+
+ return {link:link}
+
+module.directive('tgProjectLogoModel', ['$parse', ProjectLogoModelDirective])
diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee
index d45b879f..feeb02f8 100644
--- a/app/coffee/modules/base.coffee
+++ b/app/coffee/modules/base.coffee
@@ -52,6 +52,9 @@ urls = {
"not-found": "/not-found"
"permission-denied": "/permission-denied"
+ "discover": "/discover"
+ "discover-search": "/discover/search"
+
"login": "/login"
"forgot-password": "/forgot-password"
"change-password": "/change-password/:token"
diff --git a/app/coffee/modules/base/navurls.coffee b/app/coffee/modules/base/navurls.coffee
index 3885b013..85fee430 100644
--- a/app/coffee/modules/base/navurls.coffee
+++ b/app/coffee/modules/base/navurls.coffee
@@ -117,7 +117,7 @@ NavigationUrlsDirective = ($navurls, $auth, $q, $location) ->
$el.on "mouseenter", (event) ->
target = $(event.currentTarget)
- if !target.data("fullUrl")
+ if !target.data("fullUrl") || $attrs.tgNavGetParams != target.data("params")
parseNav($attrs.tgNav, $scope).then (result) ->
[name, options] = result
user = $auth.getUser()
@@ -131,6 +131,8 @@ NavigationUrlsDirective = ($navurls, $auth, $q, $location) ->
getURLParamsStr = $.param(getURLParams)
fullUrl = "#{fullUrl}?#{getURLParamsStr}"
+ target.data("params", $attrs.tgNavGetParams)
+
target.data("fullUrl", fullUrl)
if target.is("a")
diff --git a/app/coffee/modules/common/loading.coffee b/app/coffee/modules/common/loading.coffee
index 4d58b052..2c128ba9 100644
--- a/app/coffee/modules/common/loading.coffee
+++ b/app/coffee/modules/common/loading.coffee
@@ -112,7 +112,7 @@ LoadingDirective = ($loading) ->
if showLoading
currentLoading = $loading()
.target($el)
- .timeout(50)
+ .timeout(100)
.template(template)
.scope($scope)
.start()
diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee
index 77677ca7..fda8262b 100644
--- a/app/coffee/modules/resources.coffee
+++ b/app/coffee/modules/resources.coffee
@@ -175,6 +175,9 @@ urls = {
# Application tokens
"applications": "/applications"
"application-tokens": "/application-tokens"
+
+ # Stats
+ "stats-discover": "/stats/discover"
}
# Initialize api urls service
diff --git a/app/coffee/modules/resources/projects.coffee b/app/coffee/modules/resources/projects.coffee
index cfb758d1..a1745420 100644
--- a/app/coffee/modules/resources/projects.coffee
+++ b/app/coffee/modules/resources/projects.coffee
@@ -153,6 +153,31 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) ->
return defered.promise
+ service.changeLogo = (projectId, file) ->
+ maxFileSize = $config.get("maxUploadFileSize", null)
+ if maxFileSize and file.size > maxFileSize
+ response = {
+ status: 413,
+ data: _error_message: "'#{file.name}' (#{sizeFormat(file.size)}) is too heavy for our oompa
+ loompas, try it with a smaller than (#{sizeFormat(maxFileSize)})"
+ }
+ defered = $q.defer()
+ defered.reject(response)
+ return defered.promise
+
+ data = new FormData()
+ data.append('logo', file)
+ options = {
+ transformRequest: angular.identity,
+ headers: {'Content-Type': undefined}
+ }
+ url = "#{$urls.resolve("projects")}/#{projectId}/change_logo"
+ return $http.post(url, data, {}, options)
+
+ service.removeLogo = (projectId) ->
+ url = "#{$urls.resolve("projects")}/#{projectId}/remove_logo"
+ return $http.post(url)
+
return (instance) ->
instance.projects = service
diff --git a/app/coffee/modules/user-settings/main.coffee b/app/coffee/modules/user-settings/main.coffee
index 8b203aa5..98348150 100644
--- a/app/coffee/modules/user-settings/main.coffee
+++ b/app/coffee/modules/user-settings/main.coffee
@@ -148,12 +148,12 @@ UserAvatarDirective = ($auth, $model, $rs, $confirm) ->
$auth.setUser(user)
$scope.user = user
- $el.find('.overlay').addClass('hidden')
+ $el.find('.loading-overlay').removeClass('active')
$confirm.notify('success')
onError = (response) ->
showSizeInfo() if response.status == 413
- $el.find('.overlay').addClass('hidden')
+ $el.find('.loading-overlay').removeClass('active')
$confirm.notify('error', response.data._error_message)
# Change photo
@@ -162,12 +162,12 @@ UserAvatarDirective = ($auth, $model, $rs, $confirm) ->
$el.on "change", "#avatar-field", (event) ->
if $scope.avatarAttachment
- $el.find('.overlay').removeClass('hidden')
+ $el.find('.loading-overlay').addClass("active")
$rs.userSettings.changeAvatar($scope.avatarAttachment).then(onSuccess, onError)
# Use gravatar photo
- $el.on "click", "a.use-gravatar", (event) ->
- $el.find('.overlay').removeClass('hidden')
+ $el.on "click", "a.js-use-gravatar", (event) ->
+ $el.find('.loading-overlay').addClass("active")
$rs.userSettings.removeAvatar().then(onSuccess, onError)
$scope.$on "$destroy", ->
diff --git a/app/coffee/utils.coffee b/app/coffee/utils.coffee
index 81276e6d..6ced2cc5 100644
--- a/app/coffee/utils.coffee
+++ b/app/coffee/utils.coffee
@@ -195,6 +195,14 @@ _.mixin
delete obj[key]; obj
, obj).value()
+ cartesianProduct: ->
+ _.reduceRight(
+ arguments, (a,b) ->
+ _.flatten(_.map(a, (x) -> _.map b, (y) -> [y].concat(x)), true)
+ , [ [] ])
+
+
+
isImage = (name) ->
return name.match(/\.(jpe?g|png|gif|gifv|webm)/i) != null
diff --git a/app/images/discover.png b/app/images/discover.png
new file mode 100644
index 00000000..04568fc6
Binary files /dev/null and b/app/images/discover.png differ
diff --git a/app/images/looking-for-people.png b/app/images/looking-for-people.png
new file mode 100644
index 00000000..89800164
Binary files /dev/null and b/app/images/looking-for-people.png differ
diff --git a/app/images/project-logos/project-logo-01.png b/app/images/project-logos/project-logo-01.png
new file mode 100644
index 00000000..f8491702
Binary files /dev/null and b/app/images/project-logos/project-logo-01.png differ
diff --git a/app/images/project-logos/project-logo-02.png b/app/images/project-logos/project-logo-02.png
new file mode 100644
index 00000000..c4034369
Binary files /dev/null and b/app/images/project-logos/project-logo-02.png differ
diff --git a/app/images/project-logos/project-logo-03.png b/app/images/project-logos/project-logo-03.png
new file mode 100644
index 00000000..c3f2f833
Binary files /dev/null and b/app/images/project-logos/project-logo-03.png differ
diff --git a/app/images/project-logos/project-logo-04.png b/app/images/project-logos/project-logo-04.png
new file mode 100644
index 00000000..a33c622e
Binary files /dev/null and b/app/images/project-logos/project-logo-04.png differ
diff --git a/app/images/project-logos/project-logo-05.png b/app/images/project-logos/project-logo-05.png
new file mode 100644
index 00000000..5f5afd0a
Binary files /dev/null and b/app/images/project-logos/project-logo-05.png differ
diff --git a/app/js/murmurhash3_gc.js b/app/js/murmurhash3_gc.js
new file mode 100644
index 00000000..57e0dfb6
--- /dev/null
+++ b/app/js/murmurhash3_gc.js
@@ -0,0 +1,89 @@
+/**
+ * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
+ *
+ * Copyright (c) 2011 Gary Court
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * @author Gary Court
+ * @see http://github.com/garycourt/murmurhash-js
+ * @author Austin Appleby
+ * @see http://sites.google.com/site/murmurhash/
+ *
+ * @param {string} key ASCII only
+ * @param {number} seed Positive integer only
+ * @return {number} 32-bit positive integer hash
+ */
+
+function murmurhash3_32_gc(key, seed) {
+ var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i;
+
+ remainder = key.length & 3; // key.length % 4
+ bytes = key.length - remainder;
+ h1 = seed;
+ c1 = 0xcc9e2d51;
+ c2 = 0x1b873593;
+ i = 0;
+
+ while (i < bytes) {
+ k1 =
+ ((key.charCodeAt(i) & 0xff)) |
+ ((key.charCodeAt(++i) & 0xff) << 8) |
+ ((key.charCodeAt(++i) & 0xff) << 16) |
+ ((key.charCodeAt(++i) & 0xff) << 24);
+ ++i;
+
+ k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff;
+ k1 = (k1 << 15) | (k1 >>> 17);
+ k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff;
+
+ h1 ^= k1;
+ h1 = (h1 << 13) | (h1 >>> 19);
+ h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff;
+ h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16));
+ }
+
+ k1 = 0;
+
+ switch (remainder) {
+ case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
+ case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
+ case 1: k1 ^= (key.charCodeAt(i) & 0xff);
+
+ k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
+ k1 = (k1 << 15) | (k1 >>> 17);
+ k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
+ h1 ^= k1;
+ }
+
+ h1 ^= key.length;
+
+ h1 ^= h1 >>> 16;
+ h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
+ h1 ^= h1 >>> 13;
+ h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff;
+ h1 ^= h1 >>> 16;
+
+ return h1 >>> 0;
+}
+
+
diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json
index 464e558d..2228e699 100644
--- a/app/locales/taiga/locale-en.json
+++ b/app/locales/taiga/locale-en.json
@@ -450,9 +450,18 @@
"NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)",
"TAGS": "Tags",
"DESCRIPTION": "Description",
+ "RECRUITING": "Is this project looking for people?",
+ "RECRUITING_MESSAGE": "Who are you looking for?",
+ "RECRUITING_PLACEHOLDER": "Define the profiles you are looking for",
"PUBLIC_PROJECT": "Public project",
+ "PUBLIC_PROJECT_DESC": "Users will be able to find and view your project",
"PRIVATE_PROJECT": "Private project",
- "DELETE": "Delete this project"
+ "PRIVATE_PROJECT_DESC": "By default, this project will be hidden to the public",
+ "PRIVATE_OR_PUBLIC": "What's the difference between public and private projects?",
+ "DELETE": "Delete this project",
+ "LOGO_HELP": "The image will be scaled to 80x80px.",
+ "CHANGE_LOGO": "Change logo",
+ "ACTION_USE_DEFAULT_LOGO": "Use default image"
},
"REPORTS": {
"TITLE": "Reports",
@@ -715,6 +724,10 @@
"SECTION_PROJECTS": "Projects",
"HELP": "Reorder your projects to set in the top the most used ones.
The top 10 projects will appear in the top navigation bar project list",
"PRIVATE": "Private project",
+ "LOOKING_FOR_PEOPLE": "This project is looking for people",
+ "FANS_COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}",
+ "WATCHERS_COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}",
+ "MEMBERS_COUNTER_TITLE": "{total, plural, one{one member} other{# members}}",
"STATS": {
"PROJECT": "project
points",
"DEFINED": "defined
points",
@@ -744,6 +757,7 @@
"TITLE_NEXT_PROJECT": "Show next projects",
"HELP_TITLE": "Taiga Support Page",
"HELP": "Help",
+ "HOMEPAGE": "Homepage",
"FEEDBACK_TITLE": "Send feedback",
"FEEDBACK": "Feedback",
"NOTIFICATIONS_TITLE": "Edit your notification settings",
@@ -1269,9 +1283,9 @@
}
},
"USER_PROFILE": {
- "IMAGE_HELP": "The image will be scaled to 80x80px.
",
+ "IMAGE_HELP": "The image will be scaled to 80x80px.",
"ACTION_CHANGE_IMAGE": "Change",
- "ACTION_USE_GRAVATAR": "Use gravatar image",
+ "ACTION_USE_GRAVATAR": "Use default image",
"ACTION_DELETE_ACCOUNT": "Delete Taiga account",
"CHANGE_EMAIL_SUCCESS": "Check your inbox!
We have sent a mail to your account
with the instructions to set your new address",
"CHANGE_PHOTO": "Change photo",
@@ -1429,5 +1443,33 @@
"TEXT2": "Good luck!"
}
}
+ },
+ "DISCOVER": {
+ "DISCOVER_TITLE": "Discover projects",
+ "DISCOVER_SUBTITLE": "{projects, plural, one{One public project to discover} other{# public projects to discover}}",
+ "MOST_ACTIVE": "Most active",
+ "MOST_ACTIVE_EMPTY": "There are no ACTIVE projects yet",
+ "MOST_LIKED": "Most liked",
+ "MOST_LIKED_EMPTY": "There are no LIKED projects yet",
+ "VIEW_MORE": "View more",
+ "RECRUITING": "This project is looking for people",
+ "FEATURED": "Featured Projects",
+ "EMPTY": "There are no projects to show with this search criteria.
Try again!",
+ "FILTERS": {
+ "ALL": "All",
+ "KANBAN": "Kanban",
+ "SCRUM": "Scrum",
+ "PEOPLE": "Looking for people",
+ "WEEK": "Last week",
+ "MONTH": "Last month",
+ "YEAR": "Last year",
+ "ALL_TIME": "All time",
+ "CLEAR": "Clear filters"
+ },
+ "SEARCH": {
+ "INPUT_PLACEHOLDER": "Type something...",
+ "ACTION_TITLE": "Search",
+ "RESULTS": "Search results"
+ }
}
}
diff --git a/app/modules/components/project-logo-src/project-logo-src.directive.coffee b/app/modules/components/project-logo-src/project-logo-src.directive.coffee
new file mode 100644
index 00000000..2b7b8804
--- /dev/null
+++ b/app/modules/components/project-logo-src/project-logo-src.directive.coffee
@@ -0,0 +1,77 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: project-logo.directive.coffee
+###
+
+
+IMAGES = [
+ "/#{window._version}/images/project-logos/project-logo-01.png"
+ "/#{window._version}/images/project-logos/project-logo-02.png"
+ "/#{window._version}/images/project-logos/project-logo-03.png"
+ "/#{window._version}/images/project-logos/project-logo-04.png"
+ "/#{window._version}/images/project-logos/project-logo-05.png"
+]
+
+COLORS = [
+ "rgba( 153, 214, 220, 1 )"
+ "rgba( 213, 156, 156, 1 )"
+ "rgba( 214, 161, 212, 1 )"
+ "rgba( 164, 162, 219, 1 )"
+ "rgba( 152, 224, 168, 1 )"
+]
+
+LOGOS = _.cartesianProduct(IMAGES, COLORS)
+
+
+ProjectLogoSrcDirective = ($parse) ->
+ _getDefaultProjectLogo = (project) ->
+ key = "#{project.get("slug")}-#{project.get("id")}"
+ idx = murmurhash3_32_gc(key, 42) %% LOGOS.length
+ logo = LOGOS[idx]
+
+ return { src: logo[0], color: logo[1] }
+
+ link = (scope, el, attrs) ->
+ scope.$watch "project", (project) ->
+ project = Immutable.fromJS(project) # Necesary for old code
+
+ return if not project
+
+ projectLogo = project.get('logo_small_url')
+
+ if projectLogo
+ el.attr("src", projectLogo)
+ el.css('background', "")
+ else
+ logo = _getDefaultProjectLogo(project)
+ el.attr("src", logo.src)
+ el.css('background', logo.color)
+
+ scope.$on "$destroy", -> el.off()
+
+ return {
+ link: link
+ scope: {
+ project: "=tgProjectLogoSrc"
+ }
+ }
+
+ProjectLogoSrcDirective.$inject = [
+ "$parse"
+]
+
+angular.module("taigaComponents").directive("tgProjectLogoSrc", ProjectLogoSrcDirective)
diff --git a/app/modules/components/vote-button/vote-button.jade b/app/modules/components/vote-button/vote-button.jade
index 457f966b..24d63b04 100644
--- a/app/modules/components/vote-button/vote-button.jade
+++ b/app/modules/components/vote-button/vote-button.jade
@@ -16,7 +16,6 @@ a.vote-inner(
) {{ vm.item.total_voters }}
//- Anonymous user button
-
span.vote-inner(ng-if="::!vm.user")
span.track-icon
include ../../../svg/upvote.svg
diff --git a/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.coffee b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.coffee
new file mode 100644
index 00000000..ebecd4ab
--- /dev/null
+++ b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.coffee
@@ -0,0 +1,50 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-home-order-by.controller.coffee
+###
+
+class DiscoverHomeOrderByController
+ @.$inject = [
+ '$translate'
+ ]
+
+ constructor: (@translate) ->
+ @.is_open = false
+
+ @.texts = {
+ week: @translate.instant('DISCOVER.FILTERS.WEEK'),
+ month: @translate.instant('DISCOVER.FILTERS.MONTH'),
+ year: @translate.instant('DISCOVER.FILTERS.YEAR'),
+ all: @translate.instant('DISCOVER.FILTERS.ALL_TIME')
+ }
+
+ currentText: () ->
+ return @.texts[@.currentOrderBy]
+
+ open: () ->
+ @.is_open = true
+
+ close: () ->
+ @.is_open = false
+
+ orderBy: (type) ->
+ @.currentOrderBy = type
+ @.is_open = false
+
+ @.onChange({orderBy: @.currentOrderBy})
+
+angular.module("taigaDiscover").controller("DiscoverHomeOrderBy", DiscoverHomeOrderByController)
diff --git a/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.spec.coffee b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.spec.coffee
new file mode 100644
index 00000000..db71dbb9
--- /dev/null
+++ b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.controller.spec.coffee
@@ -0,0 +1,96 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-home-order-by.controller.spec.coffee
+###
+
+describe "DiscoverHomeOrderBy", ->
+ $provide = null
+ $controller = null
+ mocks = {}
+
+ _mockTranslate = ->
+ mocks.translate = {
+ instant: sinon.stub()
+ }
+
+ $provide.value("$translate", mocks.translate)
+
+ _mocks = ->
+ module (_$provide_) ->
+ $provide = _$provide_
+
+ _mockTranslate()
+
+ return null
+
+ _inject = ->
+ inject (_$controller_) ->
+ $controller = _$controller_
+
+ _setup = ->
+ _mocks()
+ _inject()
+
+ beforeEach ->
+ module "taigaDiscover"
+
+ _setup()
+
+ it "get current search text", () ->
+ mocks.translate.instant.withArgs('DISCOVER.FILTERS.WEEK').returns('week')
+ mocks.translate.instant.withArgs('DISCOVER.FILTERS.MONTH').returns('month')
+
+ ctrl = $controller("DiscoverHomeOrderBy")
+
+ ctrl.currentOrderBy = 'week'
+ text = ctrl.currentText()
+
+ expect(text).to.be.equal('week')
+
+ ctrl.currentOrderBy = 'month'
+ text = ctrl.currentText()
+
+ expect(text).to.be.equal('month')
+
+ it "open", () ->
+ ctrl = $controller("DiscoverHomeOrderBy")
+
+ ctrl.is_open = false
+
+ ctrl.open()
+
+ expect(ctrl.is_open).to.be.true
+
+ it "close", () ->
+ ctrl = $controller("DiscoverHomeOrderBy")
+
+ ctrl.is_open = true
+
+ ctrl.close()
+
+ expect(ctrl.is_open).to.be.false
+
+ it "order by", () ->
+ ctrl = $controller("DiscoverHomeOrderBy")
+ ctrl.onChange = sinon.spy()
+
+ ctrl.orderBy('week')
+
+
+ expect(ctrl.currentOrderBy).to.be.equal('week')
+ expect(ctrl.is_open).to.be.false
+ expect(ctrl.onChange).to.have.been.calledWith({orderBy: 'week'})
diff --git a/app/modules/discover/components/discover-home-order-by/discover-home-order-by.directive.coffee b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.directive.coffee
new file mode 100644
index 00000000..2def5e12
--- /dev/null
+++ b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.directive.coffee
@@ -0,0 +1,37 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-home-order-by.directive.coffee
+###
+
+DiscoverHomeOrderByDirective = () ->
+ link = (scope, el, attrs) ->
+
+ return {
+ controller: "DiscoverHomeOrderBy",
+ controllerAs: "vm",
+ bindToController: true,
+ templateUrl: "discover/components/discover-home-order-by/discover-home-order-by.html",
+ scope: {
+ currentOrderBy: "=orderBy",
+ onChange: "&"
+ },
+ link: link
+ }
+
+DiscoverHomeOrderByDirective.$inject = []
+
+angular.module("taigaDiscover").directive("tgDiscoverHomeOrderBy", DiscoverHomeOrderByDirective)
diff --git a/app/modules/discover/components/discover-home-order-by/discover-home-order-by.jade b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.jade
new file mode 100644
index 00000000..fbaaa04c
--- /dev/null
+++ b/app/modules/discover/components/discover-home-order-by/discover-home-order-by.jade
@@ -0,0 +1,12 @@
+.filter-highlighted(ng-mouseleave="vm.close()")
+ a.current-filter(
+ href="#"
+ ng-click="vm.open()"
+ ) {{vm.currentText()}}
+ span.icon-arrow-bottom
+
+ ul.filter-list(ng-if="vm.is_open")
+ li(ng-click="vm.orderBy('week')") {{ 'DISCOVER.FILTERS.WEEK' | translate }}
+ li(ng-click="vm.orderBy('month')") {{ 'DISCOVER.FILTERS.MONTH' | translate }}
+ li(ng-click="vm.orderBy('year')") {{ 'DISCOVER.FILTERS.YEAR' | translate }}
+ li(ng-click="vm.orderBy('all')") {{ 'DISCOVER.FILTERS.ALL_TIME' | translate }}
diff --git a/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.coffee b/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.coffee
new file mode 100644
index 00000000..a7ca46c1
--- /dev/null
+++ b/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.coffee
@@ -0,0 +1,36 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search-bar.controller.coffee
+###
+
+class DiscoverSearchBarController
+ @.$inject = [
+ 'tgDiscoverProjectsService'
+ ]
+
+ constructor: (@discoverProjectsService) ->
+ taiga.defineImmutableProperty @, 'projects', () => return @discoverProjectsService.projectsCount
+
+ @discoverProjectsService.fetchStats()
+
+ selectFilter: (filter) ->
+ @.onChange({filter: filter, q: @.q})
+
+ submitFilter: ->
+ @.onChange({filter: @.filter, q: @.q})
+
+angular.module("taigaDiscover").controller("DiscoverSearchBar", DiscoverSearchBarController)
diff --git a/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.spec.coffee b/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.spec.coffee
new file mode 100644
index 00000000..36b5dd4e
--- /dev/null
+++ b/app/modules/discover/components/discover-search-bar/discover-search-bar.controller.spec.coffee
@@ -0,0 +1,72 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: doscover-search-bar.controller.spec.coffee
+###
+
+describe "DiscoverSearchBarController", ->
+ $provide = null
+ $controller = null
+ mocks = {}
+
+ _mockDiscoverProjectsService = ->
+ mocks.discoverProjectsService = {
+ fetchStats: sinon.spy()
+ }
+
+ $provide.value('tgDiscoverProjectsService', mocks.discoverProjectsService)
+
+ _inject = ->
+ inject (_$controller_) ->
+ $controller = _$controller_
+
+ _mocks = ->
+ module (_$provide_) ->
+ $provide = _$provide_
+
+ _mockDiscoverProjectsService()
+
+ return null
+
+ _setup = ->
+ _inject()
+
+ beforeEach ->
+ module "taigaDiscover"
+
+ _mocks()
+ _setup()
+
+ it "select filter", () ->
+ ctrl = $controller("DiscoverSearchBar")
+ ctrl.onChange = sinon.spy()
+ ctrl.q = 'query'
+
+ ctrl.selectFilter('text')
+
+ expect(mocks.discoverProjectsService.fetchStats).to.have.been.called;
+ expect(ctrl.onChange).to.have.been.calledWith(sinon.match({filter: 'text', q: 'query'}));
+
+ it "submit filter", () ->
+ ctrl = $controller("DiscoverSearchBar")
+ ctrl.filter = 'all'
+ ctrl.q = 'query'
+ ctrl.onChange = sinon.spy()
+
+ ctrl.submitFilter()
+
+ expect(mocks.discoverProjectsService.fetchStats).to.have.been.called;
+ expect(ctrl.onChange).to.have.been.calledWith(sinon.match({filter: 'all', q: 'query'}));
diff --git a/app/modules/discover/components/discover-search-bar/discover-search-bar.directive.coffee b/app/modules/discover/components/discover-search-bar/discover-search-bar.directive.coffee
new file mode 100644
index 00000000..9dab0fbc
--- /dev/null
+++ b/app/modules/discover/components/discover-search-bar/discover-search-bar.directive.coffee
@@ -0,0 +1,38 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search.directive.coffee
+###
+
+DiscoverSearchBarDirective = () ->
+ link = (scope, el, attrs, ctrl) ->
+
+ return {
+ controller: "DiscoverSearchBar",
+ controllerAs: "vm"
+ templateUrl: 'discover/components/discover-search-bar/discover-search-bar.html',
+ bindToController: true,
+ scope: {
+ q: "="
+ filter: "=",
+ onChange: "&"
+ },
+ link: link
+ }
+
+DiscoverSearchBarDirective.$inject = []
+
+angular.module('taigaDiscover').directive('tgDiscoverSearchBar', DiscoverSearchBarDirective)
diff --git a/app/modules/discover/components/discover-search-bar/discover-search-bar.jade b/app/modules/discover/components/discover-search-bar/discover-search-bar.jade
new file mode 100644
index 00000000..201c6bca
--- /dev/null
+++ b/app/modules/discover/components/discover-search-bar/discover-search-bar.jade
@@ -0,0 +1,71 @@
+div.discover-header
+ div.discover-header-inner
+
+ h1.title {{ 'DISCOVER.DISCOVER_TITLE' | translate }}
+
+ p.project-number(
+ ng-if="vm.projects",
+ translate="DISCOVER.DISCOVER_SUBTITLE",
+ translate-values="{ projects: '{{vm.projects}}'}"
+ translate-interpolation="messageformat"
+ )
+
+ form(ng-submit="vm.submitFilter()")
+ div.searchbox
+ input(
+ name="search"
+ type="text"
+ placeholder="{{ 'DISCOVER.SEARCH.INPUT_PLACEHOLDER' | translate }}"
+ ng-model="vm.q"
+ )
+ a.search-button(
+ ng-click="vm.submitFilter()"
+ href="#"
+ title="{{ 'DISCOVER.SEARCH.ACTION_TITLE' | translate }}"
+ )
+ include ../../../../svg/search.svg
+
+ fieldset.searchbox-filters(ng-if="vm.filter")
+ input(
+ type='radio'
+ id="filter-all"
+ name="filter-search"
+ )
+ label(
+ for="filter-all"
+ ng-click="vm.selectFilter('all')"
+ ng-class="{active: vm.filter == 'all'}",
+ ) {{ 'DISCOVER.FILTERS.ALL' | translate }}
+
+ input(
+ type='radio'
+ id="filter-kanban"
+ name="filter-search"
+ )
+ label(
+ for="filter-kanban"
+ ng-class="{active: vm.filter == 'kanban'}",
+ ng-click="vm.selectFilter('kanban')"
+ ) {{ 'DISCOVER.FILTERS.KANBAN' | translate }}
+
+ input(
+ type='radio'
+ id="filter-scrum"
+ name="filter-search"
+ )
+ label(
+ for="filter-scrum"
+ ng-class="{active: vm.filter == 'scrum'}",
+ ng-click="vm.selectFilter('scrum')"
+ ) {{ 'DISCOVER.FILTERS.SCRUM' | translate }}
+
+ input(
+ type='radio'
+ id="filter-people"
+ name="filter-search"
+ )
+ label(
+ for="filter-people"
+ ng-class="{active: vm.filter == 'people'}",
+ ng-click="vm.selectFilter('people')"
+ ) {{ 'DISCOVER.FILTERS.PEOPLE' | translate }}
diff --git a/app/modules/discover/components/discover-search-bar/discover-search-bar.scss b/app/modules/discover/components/discover-search-bar/discover-search-bar.scss
new file mode 100644
index 00000000..a45705b2
--- /dev/null
+++ b/app/modules/discover/components/discover-search-bar/discover-search-bar.scss
@@ -0,0 +1,54 @@
+.discover-header {
+ background: url('../images/discover.png') repeat-x bottom left $whitish;
+ margin-bottom: 2.5rem;
+ padding: 1rem 1rem 2rem;
+ text-align: center;
+ .discover-header-inner {
+ @include centered;
+ margin: 0 auto;
+ }
+ .title {
+ @extend %xxlarge;
+ margin-bottom: 0;
+ }
+ .project-number {
+ @extend %light;
+ @extend %large;
+ color: $primary;
+ }
+ form {
+ margin: 0 30%;
+ position: relative;
+ @include breakpoint(tablet) {
+ margin: 0 .5rem;
+ }
+ }
+ input[type="text"] {
+ background: $white;
+ border: 0;
+ padding: 1rem;
+ width: 100%;
+ &:focus {
+ outline-color: $primary-light;
+ }
+ &:-webkit-autofill {
+ background: rgba($primary-dark, .5);
+ }
+ }
+ .search-button {
+ position: absolute;
+ right: 1rem;
+ top: 1rem;
+ &:hover {
+ svg {
+ fill: $primary;
+ }
+ }
+ }
+ svg {
+ fill: $gray-light;
+ height: 1.5rem;
+ transition: all .2;
+ width: 1.5rem;
+ }
+}
diff --git a/app/modules/discover/components/discover-search-list-header/discover-search-list-header.controller.coffee b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.controller.coffee
new file mode 100644
index 00000000..4c6e0c23
--- /dev/null
+++ b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.controller.coffee
@@ -0,0 +1,46 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search-list-header.controller.coffee
+###
+
+class DiscoverSearchListHeaderController
+ @.$inject = []
+
+ constructor: () ->
+ @.like_is_open = @.orderBy.indexOf('-total_fans') == 0
+ @.activity_is_open = @.orderBy.indexOf('-total_activity') == 0
+
+ openLike: () ->
+ @.like_is_open = true
+ @.activity_is_open = false
+
+ @.setOrderBy('-total_fans_last_week')
+
+ openActivity: () ->
+ @.activity_is_open = true
+ @.like_is_open = false
+
+ @.setOrderBy('-total_activity_last_week')
+
+ setOrderBy: (type = '') ->
+ if !type
+ @.like_is_open = false
+ @.activity_is_open = false
+
+ @.onChange({orderBy: type})
+
+angular.module("taigaDiscover").controller("DiscoverSearchListHeader", DiscoverSearchListHeaderController)
diff --git a/app/modules/discover/components/discover-search-list-header/discover-search-list-header.controller.spec.coffee b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.controller.spec.coffee
new file mode 100644
index 00000000..bda659f9
--- /dev/null
+++ b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.controller.spec.coffee
@@ -0,0 +1,117 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search-list-header.controller.spec.coffee
+###
+
+describe "DiscoverSearchListHeader", ->
+ $provide = null
+ $controller = null
+ scope = null
+
+ _inject = ->
+ inject (_$controller_, $rootScope) ->
+ $controller = _$controller_
+ scope = $rootScope.$new()
+
+ _setup = ->
+ _inject()
+
+ beforeEach ->
+ module "taigaDiscover"
+
+ _setup()
+
+ it "openLike", () ->
+ ctrl = $controller("DiscoverSearchListHeader", scope, {
+ orderBy: ''
+ })
+
+ ctrl.like_is_open = false
+ ctrl.activity_is_open = true
+ ctrl.setOrderBy = sinon.spy()
+
+ ctrl.openLike()
+
+ expect(ctrl.like_is_open).to.be.true
+ expect(ctrl.activity_is_open).to.be.false
+ expect(ctrl.setOrderBy).have.been.calledWith('-total_fans_last_week')
+
+ it "openActivity", () ->
+ ctrl = $controller("DiscoverSearchListHeader", scope, {
+ orderBy: ''
+ })
+
+ ctrl.activity_is_open = false
+ ctrl.like_is_open = true
+ ctrl.setOrderBy = sinon.spy()
+
+ ctrl.openActivity()
+
+ expect(ctrl.activity_is_open).to.be.true
+ expect(ctrl.like_is_open).to.be.false
+ expect(ctrl.setOrderBy).have.been.calledWith('-total_activity_last_week')
+
+ it "setOrderBy", () ->
+ ctrl = $controller("DiscoverSearchListHeader", scope, {
+ orderBy: ''
+ })
+
+ ctrl.onChange = sinon.spy()
+
+ ctrl.setOrderBy("type1")
+
+ expect(ctrl.onChange).to.have.been.calledWith(sinon.match({orderBy: "type1"}))
+
+ it "setOrderBy falsy close the like or activity layer", () ->
+ ctrl = $controller("DiscoverSearchListHeader", scope, {
+ orderBy: ''
+ })
+
+ ctrl.like_is_open = true
+ ctrl.activity_is_open = true
+
+ ctrl.onChange = sinon.spy()
+
+ ctrl.setOrderBy()
+
+ expect(ctrl.onChange).to.have.been.calledWith(sinon.match({orderBy: ''}))
+ expect(ctrl.like_is_open).to.be.false
+ expect(ctrl.activity_is_open).to.be.false
+
+ it "closed like & activity", () ->
+ ctrl = $controller("DiscoverSearchListHeader", scope, {
+ orderBy: ''
+ })
+
+ expect(ctrl.like_is_open).to.be.false
+ expect(ctrl.activity_is_open).to.be.false
+
+ it "open like", () ->
+ ctrl = $controller("DiscoverSearchListHeader", scope, {
+ orderBy: '-total_fans'
+ })
+
+ expect(ctrl.like_is_open).to.be.true
+ expect(ctrl.activity_is_open).to.be.false
+
+ it "open activity", () ->
+ ctrl = $controller("DiscoverSearchListHeader", scope, {
+ orderBy: '-total_activity'
+ })
+
+ expect(ctrl.like_is_open).to.be.false
+ expect(ctrl.activity_is_open).to.be.true
diff --git a/app/modules/discover/components/discover-search-list-header/discover-search-list-header.directive.coffee b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.directive.coffee
new file mode 100644
index 00000000..ce344946
--- /dev/null
+++ b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.directive.coffee
@@ -0,0 +1,37 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search-list-header.directive.coffee
+###
+
+DiscoverSearchListHeaderDirective = () ->
+ link = (scope, el, attrs) ->
+
+ return {
+ controller: "DiscoverSearchListHeader",
+ controllerAs: "vm",
+ bindToController: true,
+ templateUrl: "discover/components/discover-search-list-header/discover-search-list-header.html",
+ scope: {
+ onChange: "&",
+ orderBy: "="
+ },
+ link: link
+ }
+
+DiscoverSearchListHeaderDirective.$inject = []
+
+angular.module("taigaDiscover").directive("tgDiscoverSearchListHeader", DiscoverSearchListHeaderDirective)
diff --git a/app/modules/discover/components/discover-search-list-header/discover-search-list-header.jade b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.jade
new file mode 100644
index 00000000..b480a619
--- /dev/null
+++ b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.jade
@@ -0,0 +1,89 @@
+.discover-results-header
+ .discover-results-header-inner
+ .title
+ include ../../../../svg/search.svg
+ h2 {{ 'DISCOVER.SEARCH.RESULTS' | translate }}
+
+ .filter-discover-search(ng-mouseleave="vm.toggleClose()")
+ a.discover-search-filter(
+ href="#"
+ ng-click="vm.openLike()"
+ ng-class="{active: vm.like_is_open}"
+ )
+ include ../../../../svg/like.svg
+ span {{ 'DISCOVER.MOST_LIKED' | translate }}
+ a.discover-search-filter(
+ href="#"
+ ng-click="vm.openActivity()"
+ ng-class="{active: vm.activity_is_open}"
+ )
+ include ../../../../svg/activity.svg
+ span {{ 'DISCOVER.MOST_ACTIVE' | translate }}
+
+ .discover-search-subfilter.most-liked-subfilter(ng-if="vm.like_is_open")
+ a.results(
+ ng-if="vm.orderBy"
+ title=""
+ href="#",
+ ng-click="vm.setOrderBy()"
+ ) {{ 'DISCOVER.FILTERS.CLEAR' | translate }}
+
+ ul.filter-list
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_fans_last_week'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_fans_last_week')"
+ ) {{ 'DISCOVER.FILTERS.WEEK' | translate }}
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_fans_last_month'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_fans_last_month')"
+ ) {{ 'DISCOVER.FILTERS.MONTH' | translate }}
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_fans_last_year'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_fans_last_year')"
+ ) {{ 'DISCOVER.FILTERS.YEAR' | translate }}
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_fans'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_fans')"
+ ) {{ 'DISCOVER.FILTERS.ALL_TIME' | translate }}
+
+ .discover-search-subfilter.most-active-subfilter(ng-if="vm.activity_is_open")
+ a.results(
+ ng-if="vm.orderBy"
+ title=""
+ href="#",
+ ng-click="vm.setOrderBy()"
+ ) {{ 'DISCOVER.FILTERS.CLEAR' | translate }}
+
+ ul.filter-list
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_activity_last_week'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_activity_last_week')"
+ ) {{ 'DISCOVER.FILTERS.WEEK' | translate }}
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_activity_last_month'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_activity_last_month')"
+ ) {{ 'DISCOVER.FILTERS.MONTH' | translate }}
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_activity_last_year'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_activity_last_year')"
+ ) {{ 'DISCOVER.FILTERS.YEAR' | translate }}
+ li
+ a(
+ ng-class="{active: vm.orderBy == '-total_activity'}",
+ href="#",
+ ng-click="vm.setOrderBy('-total_activity')"
+ ) {{ 'DISCOVER.FILTERS.ALL_TIME' | translate }}
diff --git a/app/modules/discover/components/discover-search-list-header/discover-search-list-header.scss b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.scss
new file mode 100644
index 00000000..d3112c54
--- /dev/null
+++ b/app/modules/discover/components/discover-search-list-header/discover-search-list-header.scss
@@ -0,0 +1,80 @@
+.discover-results-header {
+ .discover-results-header-inner {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ }
+ svg {
+ @include svg-size(1.1rem);
+ fill: $gray-light;
+ }
+ .title {
+ @extend %bold;
+ @extend %larger;
+ text-transform: uppercase;
+ }
+ h2 {
+ display: inline-block;
+ }
+}
+
+.filter-discover-search {
+ .discover-search-filter {
+ margin-right: 1rem;
+ &.active {
+ color: $primary;
+ }
+ }
+}
+
+.discover-search-subfilter {
+ @include arrow('bottom', $whitish, $whitish, 1, 8);
+ align-items: center;
+ background: $whitish;
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ &.most-liked-subfilter {
+ &::after,
+ &::before {
+ left: 85%;
+ }
+ }
+ &.most-active-subfilter {
+ &::after,
+ &::before {
+ left: 95%;
+ }
+ }
+ &.ng-enter {
+ animation: dropdownFade .2s;
+ }
+ .results {
+ @extend %small;
+ color: $red-light;
+ display: block;
+ padding: .5rem 1rem;
+ transition: all .2s;
+ &:hover {
+ color: $red;
+ }
+ }
+ .filter-list {
+ display: flex;
+ margin: 0;
+ margin-left: auto;
+ a {
+ display: block;
+ padding: .5rem 1rem;
+ transition: all .2s;
+ &:hover {
+ background: $gray-light;
+ color: currentColor;
+ }
+ &.active {
+ background: $primary-light;
+ color: $whitish;
+ }
+ }
+ }
+}
diff --git a/app/modules/discover/components/featured-projects/featured-projects.controller.coffee b/app/modules/discover/components/featured-projects/featured-projects.controller.coffee
new file mode 100644
index 00000000..9175a524
--- /dev/null
+++ b/app/modules/discover/components/featured-projects/featured-projects.controller.coffee
@@ -0,0 +1,30 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: featured-projects.controller.coffee
+###
+
+class FeaturedProjectsController
+ @.$inject = [
+ "tgDiscoverProjectsService"
+ ]
+
+ constructor: (@discoverProjectsService) ->
+ taiga.defineImmutableProperty @, "featured", () => return @discoverProjectsService.featured
+
+ @discoverProjectsService.fetchFeatured()
+
+angular.module("taigaDiscover").controller("FeaturedProjects", FeaturedProjectsController)
diff --git a/app/modules/discover/components/featured-projects/featured-projects.directive.coffee b/app/modules/discover/components/featured-projects/featured-projects.directive.coffee
new file mode 100644
index 00000000..0ec079b5
--- /dev/null
+++ b/app/modules/discover/components/featured-projects/featured-projects.directive.coffee
@@ -0,0 +1,33 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: featured-projects.directive.coffee
+###
+
+FeaturedProjectsDirective = () ->
+ link = (scope, el, attrs) ->
+
+ return {
+ controller: "FeaturedProjects"
+ controllerAs: "vm",
+ templateUrl: "discover/components/featured-projects/featured-projects.html",
+ scope: {},
+ link: link
+ }
+
+FeaturedProjectsDirective.$inject = []
+
+angular.module("taigaDiscover").directive("tgFeaturedProjects", FeaturedProjectsDirective)
diff --git a/app/modules/discover/components/featured-projects/featured-projects.jade b/app/modules/discover/components/featured-projects/featured-projects.jade
new file mode 100644
index 00000000..ec1de366
--- /dev/null
+++ b/app/modules/discover/components/featured-projects/featured-projects.jade
@@ -0,0 +1,52 @@
+.featured-projects(ng-if="vm.featured.size")
+ h1.title {{ 'DISCOVER.FEATURED' | translate }}
+
+ .featured-projects-inner
+ .featured-project(tg-repeat="project in vm.featured track by project.get('id')")
+ .tags-container
+ .project-tag(
+ style="background: {{tag.get('color')}}"
+ title="{{tag.get('name')}}"
+ tg-repeat="tag in project.get('colorized_tags') track by tag.get('name')"
+ )
+ .project-card-inner
+ .project-card-header
+ a.project-card-logo(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{::project.get('name')}}"
+ )
+ img(
+ tg-project-logo-src="::project"
+ alt="{{::project.get('name')}}"
+ )
+ h2.project-card-name
+ a(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{::project.get('name')}}"
+ ) {{::project.get('name')}}
+ span.look-for-people(
+ ng-if="project.get('is_looking_for_people')"
+ title="{{ ::project.get('looking_for_people_note') }}"
+ )
+ include ../../../../svg/recruit.svg
+ p.project-card-description {{ ::project.get('description') | limitTo:100 }}{{ ::project.get('description').length < 100 ? '' : '...'}}
+ .project-card-statistics
+ span.statistic(
+ ng-class="{'active': project.get('is_fan')}"
+ title="{{ 'PROJECT.FANS_COUNTER_TITLE'|translate:{total:project.get('total_fans')||0}:'messageformat' }}"
+ )
+ include ../../../../svg/like.svg
+ span {{::project.get('total_fans')}}
+ span.statistic(
+ ng-class="{'active': project.get('is_watcher')}"
+ title="{{ 'PROJECT.WATCHERS_COUNTER_TITLE'|translate:{total:project.get('total_watchers')||0}:'messageformat' }}"
+ )
+ include ../../../../svg/eye.svg
+ span {{::project.get('total_watchers')}}
+ span.statistic(
+ title="{{ 'PROJECT.MEMBERS_COUNTER_TITLE'|translate:{total:project.get('members').size||0}:'messageformat' }}"
+ )
+ include ../../../../svg/team.svg
+ span.statistics-num {{ ::project.get('members').size }}
diff --git a/app/modules/discover/components/featured-projects/featured-projects.scss b/app/modules/discover/components/featured-projects/featured-projects.scss
new file mode 100644
index 00000000..17e4c703
--- /dev/null
+++ b/app/modules/discover/components/featured-projects/featured-projects.scss
@@ -0,0 +1,29 @@
+@import '../../../../styles/dependencies/mixins/project-card';
+
+.featured-projects {
+ @include centered;
+ .title {
+ @extend %bold;
+ @extend %larger;
+ color: $grayer;
+ text-align: center;
+ }
+}
+.featured-projects-inner {
+ align-items: stretch;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.featured-project {
+ @include project-card;
+ display: flex;
+ flex-basis: 23%;
+ @include breakpoint(tablet) {
+ flex-basis: 45%;
+ }
+ @include breakpoint(mobile) {
+ flex-basis: 100%;
+ }
+}
diff --git a/app/modules/discover/components/highlighted/highlighted.directive.coffee b/app/modules/discover/components/highlighted/highlighted.directive.coffee
new file mode 100644
index 00000000..3fee80b4
--- /dev/null
+++ b/app/modules/discover/components/highlighted/highlighted.directive.coffee
@@ -0,0 +1,32 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: highlighted.directive.coffee
+###
+
+HighlightedDirective = () ->
+ return {
+ templateUrl: "discover/components/highlighted/highlighted.html",
+ scope: {
+ loading: "=",
+ highlighted: "=",
+ orderBy: "="
+ }
+ }
+
+HighlightedDirective.$inject = []
+
+angular.module("taigaDiscover").directive("tgHighlighted", HighlightedDirective)
diff --git a/app/modules/discover/components/highlighted/highlighted.jade b/app/modules/discover/components/highlighted/highlighted.jade
new file mode 100644
index 00000000..a7606e77
--- /dev/null
+++ b/app/modules/discover/components/highlighted/highlighted.jade
@@ -0,0 +1,57 @@
+.highlighted-projects-container
+ .loading-container(
+ tg-loading="loading"
+ ng-show="loading"
+ )
+ .highlighted-project(
+ tg-repeat="project in highlighted track by project.get('id')"
+ ng-if="!loading"
+ )
+ a.project-logo(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{::project.get('name')}}"
+ )
+ img(
+ tg-project-logo-src="::project"
+ alt="{{::project.get('name')}}"
+ )
+ .project-data-container
+ .single-project-header
+ h2.project-title
+ a(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{::project.get('name')}}"
+ ) {{::project.get('name')}}
+ span.look-for-people(
+ ng-if="project.get('is_looking_for_people')"
+ title="{{ ::project.get('looking_for_people_note') }}"
+ )
+ include ../../../../svg/recruit.svg
+ .project-statistics
+ span.statistic(
+ ng-class="{'active': project.get('is_fan')}"
+ title="{{ 'PROJECT.FANS_COUNTER_TITLE'|translate:{total:project.get('total_fans')||0}:'messageformat' }}"
+ )
+ include ../../../../svg/like.svg
+ span {{::project.get('total_fans')}}
+ span.statistic(
+ ng-class="{'active': project.get('is_watcher')}"
+ title="{{ 'PROJECT.WATCHERS_COUNTER_TITLE'|translate:{total:project.get('total_watchers')||0}:'messageformat' }}"
+ )
+ include ../../../../svg/eye.svg
+ span {{::project.get('total_watchers')}}
+ span.statistic(
+ title="{{ 'PROJECT.MEMBERS_COUNTER_TITLE'|translate:{total:project.get('members').size||0}:'messageformat' }}"
+ )
+ include ../../../../svg/team.svg
+ span.statistics-num {{ ::project.get('members').size }}
+ p.project-description {{ ::project.get('description') | limitTo:150 }}{{ ::project.get('description').length < 150 ? '' : '...'}}
+
+ a.view-more-projects.button-green(
+ ng-if="highlighted"
+ tg-nav="discover-search"
+ tg-nav-get-params="{\"order_by\": \"{{orderBy}}\"}"
+ href="#"
+ ) {{ 'DISCOVER.VIEW_MORE' | translate }}
diff --git a/app/modules/discover/components/highlighted/highlighted.scss b/app/modules/discover/components/highlighted/highlighted.scss
new file mode 100644
index 00000000..e1064406
--- /dev/null
+++ b/app/modules/discover/components/highlighted/highlighted.scss
@@ -0,0 +1,205 @@
+.highlighted {
+ @include centered;
+ display: flex;
+ justify-content: space-around;
+ margin-bottom: 4rem;
+ @include breakpoint(tablet) {
+ flex-direction: column;
+ tg-most-active {
+ margin-top: 4rem;
+ }
+ }
+ tg-most-liked,
+ tg-most-active {
+ align-content: stretch;
+ display: flex;
+ flex: 1;
+ }
+ tg-most-liked {
+ margin-right: 8%;
+ @include breakpoint(tablet) {
+ margin-right: 0;
+ }
+ }
+ .most-active,
+ .most-liked {
+ align-content: stretch;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+ .header {
+ align-items: center;
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 1rem;
+ svg {
+ @include svg-size(1.5rem);
+ fill: $gray-light;
+ margin: .5rem;
+ }
+ }
+ .title-wrapper {
+ align-items: center;
+ display: flex;
+ }
+ .title {
+ @extend %bold;
+ @extend %larger;
+ color: $grayer;
+ display: inline-block;
+ margin: 0;
+ }
+ tg-highlighted {
+ display: flex;
+ flex: 1;
+ }
+ .highlighted-projects-container {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ justify-content: flex-start;
+ }
+ .loading-container {
+ margin-top: calc(50% - 1rem);
+ }
+ .loading-spinner {
+ display: block;
+ margin: 2rem auto;
+ max-height: 3rem;
+ max-width: 3rem;
+ }
+ .view-more-projects {
+ margin-top: auto;
+ width: 100%;
+ }
+ .empty-highlighted-project {
+ border: 2px dashed $whitish;
+ padding: 2rem;
+ text-align: center;
+ svg {
+ @include svg-size(2rem);
+ display: block;
+ fill: $gray-light;
+ margin: 1rem auto;
+ }
+ span {
+ @extend %light;
+ color: $gray;
+ display: block;
+ }
+ }
+}
+
+
+.filter-highlighted {
+ position: relative;
+ .current-filter {
+ padding: 1rem;
+ span {
+ margin-left: .2rem;
+ position: relative;
+ top: .2rem;
+ }
+ }
+ .filter-list {
+ background: $black;
+ position: absolute;
+ right: 0;
+ top: 1.5rem;
+ &.ng-enter {
+ animation: dropdownFade .2s ease-in;
+ }
+ &.ng-leave {
+ animation: dropdownFade .2s ease-in;
+ animation-direction: reverse;
+ }
+ }
+ li {
+ @extend %small;
+ color: $white;
+ cursor: pointer;
+ min-width: 8rem;
+ padding: .25rem .5rem;
+ &:hover {
+ background: rgba($primary-light, .4);
+ }
+ }
+}
+
+.highlighted-project {
+ align-items: flex-start;
+ border-bottom: 1px solid $whitish;
+ display: flex;
+ flex-basis: 9rem;
+ min-height: 9rem;
+ padding: 1.5rem 0;
+ &:nth-last-child(-n+2) {
+ border-bottom: 0;
+ }
+ .project-logo {
+ flex-basis: 3rem;
+ height: auto;
+ margin-right: 1rem;
+ width: 3rem;
+ img {
+ width: 100%;
+ }
+ }
+ .project-data-container {
+ flex: 1;
+ }
+ .single-project-header {
+ align-content: center;
+ display: flex;
+ justify-content: space-between;
+ }
+ .project-title {
+ @extend %large;
+ @extend %text;
+ display: inline-block;
+ margin-bottom: .5rem;
+ a {
+ color: $primary;
+ &:hover {
+ color: $primary-light;
+ }
+ }
+ }
+ .look-for-people {
+ svg {
+ @include svg-size();
+ fill: $gray-light;
+ margin-left: .5rem;
+ }
+ }
+ .project-description {
+ @extend %small;
+ color: $gray;
+ margin-bottom: 0;
+ }
+ .project-statistics {
+ display: flex;
+ flex-basis: 140px;
+ justify-content: flex-end;
+ svg {
+ @include svg-size(.8rem);
+ fill: $gray-light;
+ }
+ .svg-eye-closed {
+ display: none;
+ }
+ }
+ .statistic {
+ @extend %small;
+ color: $gray-light;
+ display: inline-block;
+ margin-right: .5rem;
+ &.active {
+ color: $primary;
+ svg {
+ fill: $primary;
+ }
+ }
+ }
+}
diff --git a/app/modules/discover/components/most-active/most-active.controller.coffee b/app/modules/discover/components/most-active/most-active.controller.coffee
new file mode 100644
index 00000000..6c8676b7
--- /dev/null
+++ b/app/modules/discover/components/most-active/most-active.controller.coffee
@@ -0,0 +1,49 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: most-active.controller.coffee
+###
+
+class MostActiveController
+ @.$inject = [
+ "tgDiscoverProjectsService"
+ ]
+
+ constructor: (@discoverProjectsService) ->
+ taiga.defineImmutableProperty @, "highlighted", () => return @discoverProjectsService.mostActive
+
+ @.currentOrderBy = 'week'
+ @.order_by = @.getOrderBy()
+
+ fetch: () ->
+ @.loading = true
+ @.order_by = @.getOrderBy()
+
+ return @discoverProjectsService.fetchMostActive({order_by: @.order_by}).then () =>
+ @.loading = false
+
+ orderBy: (type) ->
+ @.currentOrderBy = type
+
+ @.fetch()
+
+ getOrderBy: (type) ->
+ if @.currentOrderBy == 'all'
+ return '-total_activity'
+ else
+ return '-total_activity_last_' + @.currentOrderBy
+
+angular.module("taigaDiscover").controller("MostActive", MostActiveController)
diff --git a/app/modules/discover/components/most-active/most-active.controller.spec.coffee b/app/modules/discover/components/most-active/most-active.controller.spec.coffee
new file mode 100644
index 00000000..da7258bc
--- /dev/null
+++ b/app/modules/discover/components/most-active/most-active.controller.spec.coffee
@@ -0,0 +1,79 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: most-active.controller.spec.coffee
+###
+
+describe "MostActive", ->
+ $provide = null
+ $controller = null
+ mocks = {}
+
+ _mockDiscoverProjectsService = ->
+ mocks.discoverProjectsService = {
+ fetchMostActive: sinon.stub()
+ }
+
+ $provide.value("tgDiscoverProjectsService", mocks.discoverProjectsService)
+
+ _mocks = ->
+ module (_$provide_) ->
+ $provide = _$provide_
+
+ _mockDiscoverProjectsService()
+
+ return null
+
+ _inject = ->
+ inject (_$controller_) ->
+ $controller = _$controller_
+
+ _setup = ->
+ _mocks()
+ _inject()
+
+ beforeEach ->
+ module "taigaDiscover"
+
+ _setup()
+
+ it "fetch", (done) ->
+ ctrl = $controller("MostActive")
+
+ ctrl.getOrderBy = sinon.stub().returns('week')
+
+ mockPromise = mocks.discoverProjectsService.fetchMostActive.withArgs(sinon.match({order_by: 'week'})).promise()
+
+ promise = ctrl.fetch()
+
+ expect(ctrl.loading).to.be.true
+
+ mockPromise.resolve()
+
+ promise.finally () ->
+ expect(ctrl.loading).to.be.false
+ done()
+
+
+ it "order by", () ->
+ ctrl = $controller("MostActive")
+
+ ctrl.fetch = sinon.spy()
+
+ ctrl.orderBy('month')
+
+ expect(ctrl.fetch).to.have.been.called
+ expect(ctrl.currentOrderBy).to.be.equal('month')
diff --git a/app/modules/discover/components/most-active/most-active.directive.coffee b/app/modules/discover/components/most-active/most-active.directive.coffee
new file mode 100644
index 00000000..c3cf6b84
--- /dev/null
+++ b/app/modules/discover/components/most-active/most-active.directive.coffee
@@ -0,0 +1,34 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: most-active.directive.coffee
+###
+
+MostActiveDirective = () ->
+ link = (scope, el, attrs, ctrl) ->
+ ctrl.fetch()
+
+ return {
+ controller: "MostActive"
+ controllerAs: "vm",
+ templateUrl: "discover/components/most-active/most-active.html",
+ scope: {},
+ link: link
+ }
+
+MostActiveDirective.$inject = []
+
+angular.module("taigaDiscover").directive("tgMostActive", MostActiveDirective)
diff --git a/app/modules/discover/components/most-active/most-active.jade b/app/modules/discover/components/most-active/most-active.jade
new file mode 100644
index 00000000..37e58cdf
--- /dev/null
+++ b/app/modules/discover/components/most-active/most-active.jade
@@ -0,0 +1,18 @@
+.most-active(ng-if="vm.highlighted.size")
+ .header
+ .title-wrapper
+ include ../../../../svg/activity.svg
+ h1.title {{ 'DISCOVER.MOST_ACTIVE' | translate }}
+ tg-discover-home-order-by(on-change="vm.orderBy(orderBy)", order-by="vm.currentOrderBy")
+
+ tg-highlighted(
+ loading="vm.loading",
+ highlighted="vm.highlighted"
+ order-by="vm.order_by"
+ )
+
+.empty-highlighted-project(
+ ng-if="!vm.highlighted.size"
+)
+ include ../../../../svg/activity.svg
+ span {{ 'DISCOVER.MOST_ACTIVE_EMPTY' | translate }}
diff --git a/app/modules/discover/components/most-liked/most-liked.controller.coffee b/app/modules/discover/components/most-liked/most-liked.controller.coffee
new file mode 100644
index 00000000..b75e2c9a
--- /dev/null
+++ b/app/modules/discover/components/most-liked/most-liked.controller.coffee
@@ -0,0 +1,49 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: msot-liked.controller.coffee
+###
+
+class MostLikedController
+ @.$inject = [
+ "tgDiscoverProjectsService"
+ ]
+
+ constructor: (@discoverProjectsService) ->
+ taiga.defineImmutableProperty @, "highlighted", () => return @discoverProjectsService.mostLiked
+
+ @.currentOrderBy = 'week'
+ @.order_by = @.getOrderBy()
+
+ fetch: () ->
+ @.loading = true
+ @.order_by = @.getOrderBy()
+
+ @discoverProjectsService.fetchMostLiked({order_by: @.order_by}).then () =>
+ @.loading = false
+
+ orderBy: (type) ->
+ @.currentOrderBy = type
+
+ @.fetch()
+
+ getOrderBy: () ->
+ if @.currentOrderBy == 'all'
+ return '-total_fans'
+ else
+ return '-total_fans_last_' + @.currentOrderBy
+
+angular.module("taigaDiscover").controller("MostLiked", MostLikedController)
diff --git a/app/modules/discover/components/most-liked/most-liked.controller.spec.coffee b/app/modules/discover/components/most-liked/most-liked.controller.spec.coffee
new file mode 100644
index 00000000..f00ee3ad
--- /dev/null
+++ b/app/modules/discover/components/most-liked/most-liked.controller.spec.coffee
@@ -0,0 +1,79 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: most-liked.controller.spec.coffee
+###
+
+describe "MostLiked", ->
+ $provide = null
+ $controller = null
+ mocks = {}
+
+ _mockDiscoverProjectsService = ->
+ mocks.discoverProjectsService = {
+ fetchMostLiked: sinon.stub()
+ }
+
+ $provide.value("tgDiscoverProjectsService", mocks.discoverProjectsService)
+
+ _mocks = ->
+ module (_$provide_) ->
+ $provide = _$provide_
+
+ _mockDiscoverProjectsService()
+
+ return null
+
+ _inject = ->
+ inject (_$controller_) ->
+ $controller = _$controller_
+
+ _setup = ->
+ _mocks()
+ _inject()
+
+ beforeEach ->
+ module "taigaDiscover"
+
+ _setup()
+
+ it "fetch", (done) ->
+ ctrl = $controller("MostLiked")
+
+ ctrl.getOrderBy = sinon.stub().returns('week')
+
+ mockPromise = mocks.discoverProjectsService.fetchMostLiked.withArgs(sinon.match({order_by: 'week'})).promise()
+
+ promise = ctrl.fetch()
+
+ expect(ctrl.loading).to.be.true
+
+ mockPromise.resolve()
+
+ promise.finally () ->
+ expect(ctrl.loading).to.be.false
+ done()
+
+
+ it "order by", () ->
+ ctrl = $controller("MostLiked")
+
+ ctrl.fetch = sinon.spy()
+
+ ctrl.orderBy('month')
+
+ expect(ctrl.fetch).to.have.been.called
+ expect(ctrl.currentOrderBy).to.be.equal('month')
diff --git a/app/modules/discover/components/most-liked/most-liked.directive.coffee b/app/modules/discover/components/most-liked/most-liked.directive.coffee
new file mode 100644
index 00000000..06813cb9
--- /dev/null
+++ b/app/modules/discover/components/most-liked/most-liked.directive.coffee
@@ -0,0 +1,34 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: most-liked.directive.coffee
+###
+
+MostLikedDirective = () ->
+ link = (scope, el, attrs, ctrl) ->
+ ctrl.fetch()
+
+ return {
+ controller: "MostLiked"
+ controllerAs: "vm",
+ templateUrl: "discover/components/most-liked/most-liked.html",
+ scope: {},
+ link: link
+ }
+
+MostLikedDirective.$inject = []
+
+angular.module("taigaDiscover").directive("tgMostLiked", MostLikedDirective)
diff --git a/app/modules/discover/components/most-liked/most-liked.jade b/app/modules/discover/components/most-liked/most-liked.jade
new file mode 100644
index 00000000..e3967aa0
--- /dev/null
+++ b/app/modules/discover/components/most-liked/most-liked.jade
@@ -0,0 +1,17 @@
+.most-liked(ng-if="vm.highlighted.size")
+ .header
+ .title-wrapper
+ include ../../../../svg/like.svg
+ h1.title {{ 'DISCOVER.MOST_LIKED' | translate }}
+ tg-discover-home-order-by(on-change="vm.orderBy(orderBy)", order-by="vm.currentOrderBy")
+ tg-highlighted(
+ loading="vm.loading",
+ highlighted="vm.highlighted"
+ order-by="vm.order_by"
+ )
+
+.empty-highlighted-project(
+ ng-if="!vm.highlighted.size"
+)
+ include ../../../../svg/like.svg
+ span {{ 'DISCOVER.MOST_LIKED_EMPTY' | translate }}
diff --git a/app/modules/discover/discover-home/discover-home.controller.coffee b/app/modules/discover/discover-home/discover-home.controller.coffee
new file mode 100644
index 00000000..d9d249d6
--- /dev/null
+++ b/app/modules/discover/discover-home/discover-home.controller.coffee
@@ -0,0 +1,33 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-home.controller.coffee
+###
+
+class DiscoverHomeController
+ @.$inject = [
+ '$tgLocation',
+ '$tgNavUrls'
+ ]
+
+ constructor: (@location, @navUrls) ->
+
+ onSubmit: (q) ->
+ url = @navUrls.resolve('discover-search')
+
+ @location.search('text', q).path(url)
+
+angular.module("taigaDiscover").controller("DiscoverHome", DiscoverHomeController)
diff --git a/app/modules/discover/discover-home/discover-home.controller.spec.coffee b/app/modules/discover/discover-home/discover-home.controller.spec.coffee
new file mode 100644
index 00000000..0318c6ba
--- /dev/null
+++ b/app/modules/discover/discover-home/discover-home.controller.spec.coffee
@@ -0,0 +1,71 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: doscover-home.controller.spec.coffee
+###
+
+describe "DiscoverHomeController", ->
+ $provide = null
+ $controller = null
+ mocks = {}
+
+ _mockLocation = ->
+ mocks.location = {}
+
+ $provide.value('$tgLocation', mocks.location)
+
+ _mockNavUrls = ->
+ mocks.navUrls = {}
+
+ $provide.value('$tgNavUrls', mocks.navUrls)
+
+ _inject = ->
+ inject (_$controller_) ->
+ $controller = _$controller_
+
+ _mocks = ->
+ module (_$provide_) ->
+ $provide = _$provide_
+
+ _mockLocation()
+ _mockNavUrls()
+
+ return null
+
+ _setup = ->
+ _inject()
+
+ beforeEach ->
+ module "taigaDiscover"
+
+ _mocks()
+ _setup()
+
+ it "onSubmit redirect to discover search", () ->
+ mocks.navUrls.resolve = sinon.stub().withArgs('discover-search').returns('url')
+
+ pathSpy = sinon.spy()
+ searchStub = {
+ path: pathSpy
+ }
+
+ mocks.location.search = sinon.stub().withArgs('text', 'query').returns(searchStub)
+
+ ctrl = $controller("DiscoverHome")
+
+ ctrl.onSubmit('query')
+
+ expect(pathSpy).to.have.been.calledWith('url');
diff --git a/app/modules/discover/discover-home/discover-home.jade b/app/modules/discover/discover-home/discover-home.jade
new file mode 100644
index 00000000..56096cb8
--- /dev/null
+++ b/app/modules/discover/discover-home/discover-home.jade
@@ -0,0 +1,12 @@
+doctype html
+
+section.discover
+ header
+ tg-discover-search-bar(on-change="vm.onSubmit(q)")
+
+ section.highlighted
+ tg-most-liked
+ tg-most-active
+
+ section.featured-projects
+ tg-featured-projects
diff --git a/app/modules/discover/discover-search/discover-search.controller.coffee b/app/modules/discover/discover-search/discover-search.controller.coffee
new file mode 100644
index 00000000..0744a9ad
--- /dev/null
+++ b/app/modules/discover/discover-search/discover-search.controller.coffee
@@ -0,0 +1,114 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search.controller.coffee
+###
+
+class DiscoverSearchController
+ @.$inject = [
+ '$routeParams',
+ 'tgDiscoverProjectsService',
+ '$route'
+ ]
+
+ constructor: (@routeParams, @discoverProjectsService, @route) ->
+ @.page = 1
+
+ taiga.defineImmutableProperty @, "searchResult", () => return @discoverProjectsService.searchResult
+ taiga.defineImmutableProperty @, "nextSearchPage", () => return @discoverProjectsService.nextSearchPage
+
+ @.q = @routeParams.text
+ @.filter = @routeParams.filter || 'all'
+ @.orderBy = @routeParams['order_by'] || ''
+
+ @.loadingGlobal = false
+ @.loadingList = false
+ @.loadingPagination = false
+
+ fetch: () ->
+ @.page = 1
+
+ @discoverProjectsService.resetSearchList()
+
+ return @.search()
+
+ fetchByGlobalSearch: () ->
+ return if @.loadingGlobal
+
+ @.loadingGlobal = true
+
+ @.fetch().then () => @.loadingGlobal = false
+
+ fetchByOrderBy: () ->
+ return if @.loadingList
+
+ @.loadingList = true
+
+ @.fetch().then () => @.loadingList = false
+
+ showMore: () ->
+ return if @.loadingPagination
+
+ @.loadingPagination = true
+
+ @.page++
+
+ return @.search().then () => @.loadingPagination = false
+
+ search: () ->
+ filter = @.getFilter()
+
+ params = {
+ page: @.page,
+ q: @.q,
+ order_by: @.orderBy
+ }
+
+ _.assign(params, filter)
+
+ return @discoverProjectsService.fetchSearch(params)
+
+ getFilter: () ->
+ if @.filter == 'people'
+ return {is_looking_for_people: true}
+ else if @.filter == 'scrum'
+ return {is_backlog_activated: true}
+ else if @.filter == 'kanban'
+ return {is_kanban_activated: true}
+
+ return {}
+
+ onChangeFilter: (filter, q) ->
+ @.filter = filter
+ @.q = q
+
+ @route.updateParams({
+ filter: @.filter,
+ text: @.q
+ })
+
+ @.fetchByGlobalSearch()
+
+ onChangeOrder: (orderBy) ->
+ @.orderBy = orderBy
+
+ @route.updateParams({
+ order_by: orderBy
+ })
+
+ @.fetchByOrderBy()
+
+angular.module("taigaDiscover").controller("DiscoverSearch", DiscoverSearchController)
diff --git a/app/modules/discover/discover-search/discover-search.controller.spec.coffee b/app/modules/discover/discover-search/discover-search.controller.spec.coffee
new file mode 100644
index 00000000..6c05d661
--- /dev/null
+++ b/app/modules/discover/discover-search/discover-search.controller.spec.coffee
@@ -0,0 +1,199 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search.controller.spec.coffee
+###
+
+describe "DiscoverSearch", ->
+ $provide = null
+ $controller = null
+ mocks = {}
+
+ _mockRouteParams = ->
+ mocks.routeParams = {}
+
+ $provide.value("$routeParams", mocks.routeParams)
+
+ _mockRoute = ->
+ mocks.route = {}
+
+ $provide.value("$route", mocks.route)
+
+ _mockDiscoverProjects = ->
+ mocks.discoverProjects = {
+ resetSearchList: sinon.spy(),
+ fetchSearch: sinon.stub()
+ }
+
+ mocks.discoverProjects.fetchSearch.promise().resolve()
+
+ $provide.value("tgDiscoverProjectsService", mocks.discoverProjects)
+
+ _mocks = ->
+ module (_$provide_) ->
+ $provide = _$provide_
+
+ _mockRoute()
+ _mockRouteParams()
+ _mockDiscoverProjects()
+
+ return null
+
+ _inject = ->
+ inject (_$controller_) ->
+ $controller = _$controller_
+
+ _setup = ->
+ _mocks()
+ _inject()
+
+ beforeEach ->
+ module "taigaDiscover"
+
+ _setup()
+
+ it "initialize search params", () ->
+ mocks.routeParams.text = 'text'
+ mocks.routeParams.filter = 'filter'
+ mocks.routeParams.order_by = 'order'
+
+ ctrl = $controller('DiscoverSearch')
+
+ expect(ctrl.q).to.be.equal('text')
+ expect(ctrl.filter).to.be.equal('filter')
+ expect(ctrl.orderBy).to.be.equal('order')
+
+ it "fetch", () ->
+ ctrl = $controller('DiscoverSearch')
+
+ ctrl.search = sinon.spy()
+
+ ctrl.fetch()
+
+ expect(mocks.discoverProjects.resetSearchList).to.have.been.called
+ expect(ctrl.search).to.have.been.called
+ expect(ctrl.page).to.be.equal(1)
+
+ it "showMore", (done) ->
+ ctrl = $controller('DiscoverSearch')
+
+ ctrl.search = sinon.stub().promise()
+
+ ctrl.showMore().then () ->
+ expect(ctrl.loadingPagination).to.be.false
+
+ done()
+
+ expect(ctrl.loadingPagination).to.be.true
+ expect(ctrl.search).to.have.been.called
+ expect(ctrl.page).to.be.equal(2)
+
+ ctrl.search.resolve()
+
+ it "search", () ->
+ mocks.discoverProjects.fetchSearch = sinon.stub()
+
+ filter = {
+ filter: '123'
+ }
+
+ ctrl = $controller('DiscoverSearch')
+
+ ctrl.page = 1
+ ctrl.q = 'text'
+ ctrl.orderBy = 1
+
+ ctrl.getFilter = () -> return filter
+
+ params = {
+ filter: '123',
+ page: 1,
+ q: 'text',
+ order_by: 1
+ }
+
+ ctrl.search()
+
+ expect(mocks.discoverProjects.fetchSearch).have.been.calledWith(sinon.match(params))
+
+ it "get filter", () ->
+ ctrl = $controller('DiscoverSearch')
+
+ ctrl.filter = 'people'
+ expect(ctrl.getFilter()).to.be.eql({is_looking_for_people: true})
+
+ ctrl.filter = 'scrum'
+ expect(ctrl.getFilter()).to.be.eql({is_backlog_activated: true})
+
+ ctrl.filter = 'kanban'
+ expect(ctrl.getFilter()).to.be.eql({is_kanban_activated: true})
+
+ it "onChangeFilter", () ->
+ ctrl = $controller('DiscoverSearch')
+
+ mocks.route.updateParams = sinon.stub()
+
+ ctrl.fetchByGlobalSearch = sinon.spy()
+
+ ctrl.onChangeFilter('filter', 'query')
+
+ expect(ctrl.filter).to.be.equal('filter')
+ expect(ctrl.q).to.be.equal('query')
+ expect(ctrl.fetchByGlobalSearch).to.have.been.called
+ expect(mocks.route.updateParams).to.have.been.calledWith(sinon.match({filter: 'filter', text: 'query'}))
+
+ it "onChangeOrder", () ->
+ ctrl = $controller('DiscoverSearch')
+
+ mocks.route.updateParams = sinon.stub()
+
+ ctrl.fetchByOrderBy = sinon.spy()
+
+ ctrl.onChangeOrder('order-by')
+
+ expect(ctrl.orderBy).to.be.equal('order-by')
+ expect(ctrl.fetchByOrderBy).to.have.been.called
+ expect(mocks.route.updateParams).to.have.been.calledWith(sinon.match({order_by: 'order-by'}))
+
+ it "fetchByGlobalSearch", (done) ->
+ ctrl = $controller('DiscoverSearch')
+
+ ctrl.fetch = sinon.stub().promise()
+
+ ctrl.fetchByGlobalSearch().then () ->
+ expect(ctrl.loadingGlobal).to.be.false
+
+ done()
+
+ expect(ctrl.loadingGlobal).to.be.true
+ expect(ctrl.fetch).to.have.been.called
+
+ ctrl.fetch.resolve()
+
+ it "fetchByOrderBy", (done) ->
+ ctrl = $controller('DiscoverSearch')
+
+ ctrl.fetch = sinon.stub().promise()
+
+ ctrl.fetchByOrderBy().then () ->
+ expect(ctrl.loadingList).to.be.false
+
+ done()
+
+ expect(ctrl.loadingList).to.be.true
+ expect(ctrl.fetch).to.have.been.called
+
+ ctrl.fetch.resolve()
diff --git a/app/modules/discover/discover-search/discover-search.directive.coffee b/app/modules/discover/discover-search/discover-search.directive.coffee
new file mode 100644
index 00000000..5f66cbea
--- /dev/null
+++ b/app/modules/discover/discover-search/discover-search.directive.coffee
@@ -0,0 +1,32 @@
+###
+# Copyright (C) 2014-2016 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-search.directive.coffee
+###
+
+DiscoverSearchDirective = () ->
+ link = (scope, element, attrs, ctrl) ->
+ ctrl.fetch()
+
+ return {
+ controller: "DiscoverSearch",
+ controllerAs: "vm"
+ link: link
+ }
+
+DiscoverSearchDirective.$inject = []
+
+angular.module("taigaDiscover").directive("tgDiscoverSearch", DiscoverSearchDirective)
diff --git a/app/modules/discover/discover-search/discover-search.jade b/app/modules/discover/discover-search/discover-search.jade
new file mode 100644
index 00000000..e7629e0b
--- /dev/null
+++ b/app/modules/discover/discover-search/discover-search.jade
@@ -0,0 +1,77 @@
+div(tg-discover-search)
+ .discover-search
+ tg-discover-search-bar(
+ filter="vm.filter",
+ q="vm.q",
+ on-change="vm.onChangeFilter(filter, q)"
+ )
+
+ .empty-discover-results(ng-if="!vm.searchResult.size && !vm.loadingGlobal && !vm.loadingList")
+ img(
+ src="/#{v}/images/issues-empty.png",
+ alt="{{ DISCOVER.EMPTY | translate }}"
+ )
+ p.title(translate="DISCOVER.EMPTY")
+
+ .discover-results(ng-if="vm.searchResult.size || vm.loadingGlobal || vm.loadingList")
+ .spin(tg-loading="vm.loadingGlobal")
+
+ .discover-results-inner(ng-if="!vm.loadingGlobal")
+ tg-discover-search-list-header(
+ on-change="vm.onChangeOrder(orderBy)",
+ order-by="vm.orderBy"
+ )
+
+ .spin(ng-show="vm.loadingList", tg-loading="vm.loadingList")
+
+ ul.project-list(ng-if="!vm.loadingList && vm.searchResult.size")
+ li.list-itemtype-project(tg-repeat="project in vm.searchResult track by project.get('id')")
+ .list-itemtype-project-left
+ a.list-itemtype-project-image(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{ ::project.get('name') }}"
+ )
+ img(
+ tg-project-logo-src="::project"
+ alt="{{::project.get('name')}}"
+ )
+ .list-itemtype-project-data
+ h2
+ a(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{ ::project.get('name') }}"
+ ) {{project.get('name')}}
+ span.look-for-people(
+ ng-if="project.get('is_looking_for_people')"
+ title="{{ ::project.get('looking_for_people_note') }}"
+ )
+ include ../../../svg/recruit.svg
+ p {{ ::project.get('description') | limitTo:300 }}
+ span(ng-if="::project.get('description').length > 300") ...
+ .list-itemtype-project-right.project-statistics
+ span.statistic(
+ ng-class="{'active': project.get('is_fan')}"
+ title="{{ 'PROJECT.FANS_COUNTER_TITLE'|translate:{total:project.get('total_fans')||0}:'messageformat' }}"
+ )
+ include ../../../svg/like.svg
+ span {{::project.get('total_fans')}}
+ span.statistic(
+ ng-class="{'active': project.get('is_watcher')}"
+ title="{{ 'PROJECT.WATCHERS_COUNTER_TITLE'|translate:{total:project.get('total_watchers')||0}:'messageformat' }}"
+ )
+ include ../../../svg/eye.svg
+ span {{::project.get('total_watchers')}}
+ span.statistic(
+ title="{{ 'PROJECT.MEMBERS_COUNTER_TITLE'|translate:{total:project.get('members').size||0}:'messageformat' }}"
+ )
+ include ../../../svg/team.svg
+ span.statistics-num {{ ::project.get('members').size }}
+
+ a.button-green.more-results(
+ tg-loading="vm.loadingPagination"
+ href="#"
+ ng-click="vm.showMore()"
+ ng-if="vm.nextSearchPage"
+ ) {{ 'DISCOVER.VIEW_MORE' | translate }}
diff --git a/app/modules/discover/discover-search/discover-search.scss b/app/modules/discover/discover-search/discover-search.scss
new file mode 100644
index 00000000..f31bcdbd
--- /dev/null
+++ b/app/modules/discover/discover-search/discover-search.scss
@@ -0,0 +1,131 @@
+.discover-search {
+ .discover-header {
+ form {
+ margin: 0 8rem;
+ position: relative;
+ }
+
+ .search-button {
+ left: 1rem;
+ right: auto;
+ }
+ .searchbox {
+ input {
+ padding-left: 3.5rem;
+ padding-right: 23rem;
+ }
+ }
+ }
+ .searchbox-filters {
+ position: absolute;
+ right: 1rem;
+ top: .7rem;
+ width: auto;
+ input {
+ display: none;
+ }
+ label {
+ border-radius: 4px;
+ color: $gray-light;
+ cursor: pointer;
+ display: inline-block;
+ padding: .4rem .75rem;
+ transition: all .2s;
+ transition-delay: .2s;
+ &.active {
+ background: $primary-light;
+ color: $white;
+ }
+ &:hover {
+ background: $whitish;
+ color: $gray;
+ }
+ }
+ }
+}
+
+.discover-results {
+ @include centered;
+ .discover-results-inner {
+ .spin {
+ margin-top: 4rem;
+ }
+ }
+ .list-itemtype-project {
+ border-bottom: 1px solid $gray-light;
+ display: flex;
+ padding: 1rem 0;
+ &:last-child {
+ border-bottom: 0;
+ }
+ }
+ .list-itemtype-project-left {
+ align-items: flex-start;
+ display: flex;
+ }
+ .list-itemtype-project-image {
+ flex-shrink: 0;
+ margin-right: 1rem;
+ }
+ .list-itemtype-project-data {
+ flex: 1;
+ vertical-align: middle;
+ }
+ .look-for-people {
+ margin-left: .5rem;
+ svg {
+ @include svg-size(1rem);
+ fill: $gray-light;
+ }
+ }
+ .project-statistics {
+ display: flex;
+ flex-basis: 140px;
+ justify-content: flex-end;
+ svg {
+ @include svg-size(.8rem);
+ fill: $gray-light;
+ }
+ .svg-eye-closed {
+ display: none;
+ }
+ }
+ .statistic {
+ @extend %small;
+ color: $gray-light;
+ display: inline-block;
+ margin-right: .5rem;
+ &.active {
+ color: $primary;
+ svg {
+ fill: $primary;
+ }
+ }
+ }
+ .more-results {
+ display: block;
+ margin: 0 20rem;
+ transition: inherit;
+ }
+ div[tg-loading] {
+ img {
+ display: block;
+ margin: 0 auto;
+ }
+ }
+}
+
+.empty-discover-results {
+ @include centered;
+ margin-top: 4rem;
+ text-align: center;
+ img {
+ margin-bottom: 1rem;
+ }
+ .title {
+ @extend %large;
+ @extend %light;
+ margin: 0;
+ text-transform: uppercase;
+ }
+}
diff --git a/app/modules/discover/discover.module.coffee b/app/modules/discover/discover.module.coffee
new file mode 100644
index 00000000..2c2bfb7f
--- /dev/null
+++ b/app/modules/discover/discover.module.coffee
@@ -0,0 +1,20 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover.module.coffee
+###
+
+module = angular.module("taigaDiscover", [])
diff --git a/app/modules/discover/services/discover-projects.service.coffee b/app/modules/discover/services/discover-projects.service.coffee
new file mode 100644
index 00000000..f53808e3
--- /dev/null
+++ b/app/modules/discover/services/discover-projects.service.coffee
@@ -0,0 +1,93 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-projects.service.coffee
+###
+
+taiga = @.taiga
+
+class DiscoverProjectsService extends taiga.Service
+ @.$inject = [
+ "tgResources",
+ "tgProjectsService"
+ ]
+
+ constructor: (@rs, @projectsService) ->
+ @._mostLiked = Immutable.List()
+ @._mostActive = Immutable.List()
+ @._featured = Immutable.List()
+ @._searchResult = Immutable.List()
+ @._projectsCount = 0
+
+ @.decorate = @projectsService._decorate.bind(@projectsService)
+
+ taiga.defineImmutableProperty @, "mostLiked", () => return @._mostLiked
+ taiga.defineImmutableProperty @, "mostActive", () => return @._mostActive
+ taiga.defineImmutableProperty @, "featured", () => return @._featured
+ taiga.defineImmutableProperty @, "searchResult", () => return @._searchResult
+ taiga.defineImmutableProperty @, "nextSearchPage", () => return @._nextSearchPage
+ taiga.defineImmutableProperty @, "projectsCount", () => return @._projectsCount
+
+ fetchMostLiked: (params) ->
+ return @rs.projects.getProjects(params, false)
+ .then (result) =>
+ data = result.data.slice(0, 5)
+
+ projects = Immutable.fromJS(data)
+ projects = projects.map(@.decorate)
+
+ @._mostLiked = projects
+
+ fetchMostActive: (params) ->
+ return @rs.projects.getProjects(params, false)
+ .then (result) =>
+ data = result.data.slice(0, 5)
+
+ projects = Immutable.fromJS(data)
+ projects = projects.map(@.decorate)
+
+ @._mostActive = projects
+
+ fetchFeatured: () ->
+ params = {is_featured: true}
+
+ return @rs.projects.getProjects(params, false)
+ .then (result) =>
+ data = result.data.slice(0, 4)
+
+ projects = Immutable.fromJS(data)
+ projects = projects.map(@.decorate)
+
+ @._featured = projects
+
+ resetSearchList: () ->
+ @._searchResult = Immutable.List()
+
+ fetchStats: () ->
+ return @rs.stats.discover().then (discover) =>
+ @._projectsCount = discover.getIn(['projects', 'total'])
+
+ fetchSearch: (params) ->
+ return @rs.projects.getProjects(params)
+ .then (result) =>
+ @._nextSearchPage = !!result.headers('X-Pagination-Next')
+
+ projects = Immutable.fromJS(result.data)
+ projects = projects.map(@.decorate)
+
+ @._searchResult = @._searchResult.concat(projects)
+
+angular.module("taigaDiscover").service("tgDiscoverProjectsService", DiscoverProjectsService)
diff --git a/app/modules/discover/services/discover-projects.service.spec.coffee b/app/modules/discover/services/discover-projects.service.spec.coffee
new file mode 100644
index 00000000..97adbd0c
--- /dev/null
+++ b/app/modules/discover/services/discover-projects.service.spec.coffee
@@ -0,0 +1,178 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: discover-projects.service.spec.coffee
+###
+
+describe "tgDiscoverProjectsService", ->
+ discoverProjectsService = provide = null
+ mocks = {}
+
+ _mockResources = () ->
+ mocks.resources = {
+ projects: {
+ getProjects: sinon.stub()
+ },
+ stats: {
+ discover: sinon.stub()
+ }
+ }
+
+ provide.value "tgResources", mocks.resources
+
+ _mockProjectsService = () ->
+ mocks.projectsService = {
+ _decorate: (content) ->
+ return content.set('decorate', true)
+ }
+
+ provide.value "tgProjectsService", mocks.projectsService
+
+ _inject = (callback) ->
+ inject (_tgDiscoverProjectsService_) ->
+ discoverProjectsService = _tgDiscoverProjectsService_
+ callback() if callback
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockResources()
+ _mockProjectsService()
+ return null
+
+ _setup = ->
+ _mocks()
+
+ beforeEach ->
+ module "taigaDiscover"
+ _setup()
+ _inject()
+
+ it "fetch most liked", (done) ->
+ params = {test: 1}
+
+ mocks.resources.projects.getProjects.withArgs(sinon.match(params), false).promise().resolve({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3},
+ {id: 4},
+ {id: 5},
+ {id: 6},
+ {id: 7}
+ ]
+ })
+
+ discoverProjectsService.fetchMostLiked(params).then () ->
+ result = discoverProjectsService._mostLiked.toJS()
+
+ expect(result).to.have.length(5)
+ expect(result[0].decorate).to.be.ok;
+
+ done()
+
+ it "fetch most active", (done) ->
+ params = {test: 1}
+
+ mocks.resources.projects.getProjects.withArgs(sinon.match(params), false).promise().resolve({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3},
+ {id: 4},
+ {id: 5},
+ {id: 6},
+ {id: 7}
+ ]
+ })
+
+ discoverProjectsService.fetchMostActive(params).then () ->
+ result = discoverProjectsService._mostActive.toJS()
+
+ expect(result).to.have.length(5)
+ expect(result[0].decorate).to.be.ok;
+
+ done()
+
+ it "fetch featured", (done) ->
+ mocks.resources.projects.getProjects.withArgs(sinon.match({is_featured: true}), false).promise().resolve({
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3},
+ {id: 4},
+ {id: 5},
+ {id: 6},
+ {id: 7}
+ ]
+ })
+
+ discoverProjectsService.fetchFeatured().then () ->
+ result = discoverProjectsService._featured.toJS()
+
+ expect(result).to.have.length(4)
+ expect(result[0].decorate).to.be.ok;
+
+ done()
+
+ it "reset search list", () ->
+ discoverProjectsService._searchResult = 'xxx'
+
+ discoverProjectsService.resetSearchList()
+
+ expect(discoverProjectsService._searchResult.size).to.be.equal(0)
+
+ it "fetch stats", (done) ->
+ mocks.resources.stats.discover.promise().resolve(Immutable.fromJS({
+ projects: {
+ total: 3
+ }
+ }))
+
+ discoverProjectsService.fetchStats().then () ->
+ expect(discoverProjectsService._projectsCount).to.be.equal(3)
+
+ done()
+
+ it "fetch search", (done) ->
+ params = {test: 1}
+
+ result = {
+ headers: sinon.stub(),
+ data: [
+ {id: 1},
+ {id: 2},
+ {id: 3}
+ ]
+ }
+
+ result.headers.withArgs('X-Pagination-Next').returns('next')
+
+ mocks.resources.projects.getProjects.withArgs(sinon.match(params)).promise().resolve(result)
+
+ discoverProjectsService._searchResult = Immutable.fromJS([
+ {id: 4},
+ {id: 5}
+ ])
+
+ discoverProjectsService.fetchSearch(params).then () ->
+ result = discoverProjectsService._searchResult.toJS()
+
+ expect(result).to.have.length(5)
+
+ expect(result[4].decorate).to.be.ok;
+
+ done()
diff --git a/app/modules/home/home-controller.spec.coffee b/app/modules/home/home-controller.spec.coffee
new file mode 100644
index 00000000..2713181f
--- /dev/null
+++ b/app/modules/home/home-controller.spec.coffee
@@ -0,0 +1,78 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: home.controller.spec.coffee
+###
+
+describe "HomeController", ->
+ homeCtrl = null
+ provide = null
+ controller = null
+ mocks = {}
+
+ _mockCurrentUserService = () ->
+ mocks.currentUserService = {
+ getUser: sinon.stub()
+ }
+
+ provide.value "tgCurrentUserService", mocks.currentUserService
+
+ _mockLocation = () ->
+ mocks.location = {
+ path: sinon.stub()
+ }
+ provide.value "$location", mocks.location
+
+ _mockTgNavUrls = () ->
+ mocks.tgNavUrls = {
+ resolve: sinon.stub()
+ }
+
+ provide.value "$tgNavUrls", mocks.tgNavUrls
+
+ _mocks = () ->
+ module ($provide) ->
+ provide = $provide
+ _mockCurrentUserService()
+ _mockLocation()
+ _mockTgNavUrls()
+
+ return null
+
+ beforeEach ->
+ module "taigaHome"
+
+ _mocks()
+
+ inject ($controller) ->
+ controller = $controller
+
+ it "anonymous home", () ->
+ homeCtrl = controller "Home",
+ $scope: {}
+
+ expect(mocks.tgNavUrls.resolve).to.be.calledWith("discover")
+ expect(mocks.location.path).to.be.calledOnce
+
+ it "non anonymous home", () ->
+ mocks.currentUserService = {
+ getUser: Immutable.fromJS({
+ id: 1
+ })
+ }
+
+ expect(mocks.tgNavUrls.resolve).to.be.notCalled
+ expect(mocks.location.path).to.be.notCalled
diff --git a/app/modules/home/home.controller.coffee b/app/modules/home/home.controller.coffee
new file mode 100644
index 00000000..a3d9121e
--- /dev/null
+++ b/app/modules/home/home.controller.coffee
@@ -0,0 +1,32 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: home.controller.coffee
+###
+
+class HomeController
+ @.$inject = [
+ "tgCurrentUserService",
+ "$location",
+ "$tgNavUrls"
+ ]
+
+ constructor: (@currentUserService, @location, @navUrls) ->
+ if not @currentUserService.getUser()
+ @location.path(@navUrls.resolve("discover"))
+
+
+angular.module("taigaHome").controller("Home", HomeController)
diff --git a/app/modules/home/home.scss b/app/modules/home/home.scss
index e605acea..9f488e10 100644
--- a/app/modules/home/home.scss
+++ b/app/modules/home/home.scss
@@ -11,7 +11,7 @@
display: block;
}
.title-bar {
- @extend %title;
+ @extend %light;
@extend %larger;
align-content: center;
background: $whitish;
diff --git a/app/modules/home/projects/home-project-list.jade b/app/modules/home/projects/home-project-list.jade
index 0841af36..53336204 100644
--- a/app/modules/home/projects/home-project-list.jade
+++ b/app/modules/home/projects/home-project-list.jade
@@ -1,13 +1,59 @@
section.home-project-list(ng-if="vm.projects.size")
- ul
- li.home-project-list-single(tg-bind-scope, tg-repeat="project in vm.projects")
- a(href="#", tg-nav="project:project=project.get('slug')")
- h2.home-project-list-single-title
- span.project-name(title="{{ ::project.get('name') }}") {{::project.get('name')}}
- span.private(ng-if="project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
- include ../../../svg/lock.svg
- p {{ ::project.get('description') | limitTo:150 }}
- span(ng-if="::project.get('description').size > 150") ...
+
+ .home-project(tg-bind-scope, tg-repeat="project in vm.projects")
+ .tags-container
+ .project-tag(
+ style="background: {{tag.get('color')}}"
+ title="{{tag.get('name')}}"
+ tg-repeat="tag in project.get('colorized_tags') track by tag.get('name')"
+ )
+ .project-card-inner(href="#", tg-nav="project:project=project.get('slug')")
+ .project-card-header
+ a.project-card-logo(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{::project.get('name')}}"
+ )
+ img(
+ tg-project-logo-src="::project"
+ alt="{{::project.get('name')}}"
+ )
+ h2.project-card-name
+ a(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{::project.get('name')}}"
+ ) {{::project.get('name')}}
+ span.look-for-people(
+ ng-if="project.get('is_looking_for_people')"
+ title="{{ ::project.get('looking_for_people_note') }}"
+ )
+ include ../../../svg/recruit.svg
+ p.project-card-description {{::project.get('description')| limitTo:100 }}
+ span(ng-if="::project.get('description').length > 100") ...
+ .project-card-statistics
+ span.statistic(
+ ng-class="{'active': project.get('is_fan')}"
+ title="{{ 'PROJECT.FANS_COUNTER_TITLE'|translate:{total:project.get('total_fans')||0}:'messageformat' }}"
+ )
+ include ../../../svg/like.svg
+ span {{::project.get('total_fans')}}
+ span.statistic(
+ ng-class="{'active': project.get('is_watcher')}"
+ title="{{ 'PROJECT.WATCHERS_COUNTER_TITLE'|translate:{total:project.get('total_watchers')||0}:'messageformat' }}"
+ )
+ include ../../../svg/eye.svg
+ span {{::project.get('total_watchers')}}
+ span.statistic(
+ title="{{ 'PROJECT.MEMBERS_COUNTER_TITLE'|translate:{total:project.get('members').size||0}:'messageformat' }}"
+ )
+ include ../../../svg/team.svg
+ span.statistics-num {{ ::project.get('members').size }}
+ span.statistic(
+ ng-if="::project.get('is_private')"
+ title="{{ 'PROJECT.PRIVATE' | translate }}"
+ )
+ include ../../../svg/lock.svg
a.see-more-projects-btn.button-gray(
href="#",
diff --git a/app/modules/home/projects/home-project-list.scss b/app/modules/home/projects/home-project-list.scss
index 21d67b31..c73826b0 100644
--- a/app/modules/home/projects/home-project-list.scss
+++ b/app/modules/home/projects/home-project-list.scss
@@ -1,52 +1,13 @@
-.home-project-list {
- li {
- border: 1px solid lighten($gray-light, 15%);
- border-radius: 3px;
- cursor: pointer;
- margin-bottom: .75rem;
- padding: 1rem;
- text-overflow: ellipsis;
- &:hover {
- border-color: $primary-light;
- transition: all .3s linear;
- p {
- color: $gray;
- transition: color .3s linear;
- }
- .private path {
- fill: $gray;
- transition: fill .3s linear;
- }
- }
- a {
- display: flex;
- flex-direction: column;
- min-height: 5rem;
- }
- }
- h2 {
- @extend %text;
- color: $gray;
- font-size: 1.5rem;
- line-height: 1.3;
- margin-bottom: .5rem;
- text-transform: none;
- .project-name {
- display: inline-block;
- max-width: 90%;
- overflow: hidden;
- text-overflow: ellipsis;
- vertical-align: middle;
- white-space: nowrap;
- }
- }
- p {
- @extend %text;
- @extend %xsmall;
- color: $gray-light;
- line-height: 125%;
- margin: 0;
- word-wrap: break-word;
+@import '../../../styles/dependencies/mixins/project-card';
+
+.home-project {
+ @include project-card;
+ cursor: pointer;
+ margin-bottom: 1rem;
+ transition: .2s;
+ transition-delay: .1s;
+ &:hover {
+ border: 1px solid $primary-light;
}
}
diff --git a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee
index aeac042e..ac09365c 100644
--- a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee
+++ b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee
@@ -27,7 +27,8 @@ DropdownUserDirective = (authService, configService, locationService,
scope.vm.logout = ->
authService.logout()
- locationService.path(navUrlsService.resolve("login"))
+ locationService.url(navUrlsService.resolve("discover"))
+ locationService.search({})
scope.vm.sendFeedback = ->
feedbackService.sendFeedback()
diff --git a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee
index d85e69fd..87287d6a 100644
--- a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee
+++ b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee
@@ -50,8 +50,10 @@ describe "dropdownUserDirective", () ->
_mockTgLocation = () ->
mockTgLocation = {
- path: sinon.stub()
+ url: sinon.stub()
+ search: sinon.stub()
}
+
provide.value "$tgLocation", mockTgLocation
_mockTgNavUrls = () ->
@@ -97,16 +99,19 @@ describe "dropdownUserDirective", () ->
expect(vm.isFeedbackEnabled).to.be.equal(true)
it "dropdown user log out", () ->
- mockTgNavUrls.resolve.withArgs("login").returns("/login")
+ mockTgNavUrls.resolve.withArgs("discover").returns("/discover")
elm = createDirective()
scope.$apply()
vm = elm.isolateScope().vm
expect(mockTgAuth.logout.callCount).to.be.equal(0)
- expect(mockTgLocation.path.callCount).to.be.equal(0)
+ expect(mockTgLocation.url.callCount).to.be.equal(0)
+ expect(mockTgLocation.search.callCount).to.be.equal(0)
vm.logout()
expect(mockTgAuth.logout.callCount).to.be.equal(1)
- expect(mockTgLocation.path.callCount).to.be.equal(1)
- expect(mockTgLocation.path.calledWith("/login")).to.be.true
+ expect(mockTgLocation.url.callCount).to.be.equal(1)
+ expect(mockTgLocation.search.callCount).to.be.equal(1)
+ expect(mockTgLocation.url.calledWith("/discover")).to.be.true
+ expect(mockTgLocation.search.calledWith({})).to.be.true
it "dropdown user send feedback", () ->
elm = createDirective()
diff --git a/app/modules/navigation-bar/navigation-bar.directive.coffee b/app/modules/navigation-bar/navigation-bar.directive.coffee
index 67d4589d..c32a792f 100644
--- a/app/modules/navigation-bar/navigation-bar.directive.coffee
+++ b/app/modules/navigation-bar/navigation-bar.directive.coffee
@@ -17,12 +17,14 @@
# File: navigation-bar.directive.coffee
###
-NavigationBarDirective = (currentUserService, navigationBarService, $location) ->
+NavigationBarDirective = (currentUserService, navigationBarService,
+ locationService, navUrlsService) ->
+
link = (scope, el, attrs, ctrl) ->
scope.vm = {}
scope.$on "$routeChangeSuccess", () ->
- if $location.path() == "/"
+ if locationService.path() == "/"
scope.vm.active = true
else
scope.vm.active = false
@@ -31,6 +33,15 @@ NavigationBarDirective = (currentUserService, navigationBarService, $location) -
taiga.defineImmutableProperty(scope.vm, "isAuthenticated", () -> currentUserService.isAuthenticated())
taiga.defineImmutableProperty(scope.vm, "isEnabledHeader", () -> navigationBarService.isEnabledHeader())
+ scope.vm.login = ->
+ nextUrl = encodeURIComponent(locationService.url())
+ locationService.url(navUrlsService.resolve("login"))
+ locationService.search({next: nextUrl})
+
+ scope.vm.register = ->
+ nextUrl = encodeURIComponent(locationService.url())
+ locationService.url(navUrlsService.resolve("register"))
+ locationService.search({next: nextUrl})
directive = {
templateUrl: "navigation-bar/navigation-bar.html"
@@ -42,8 +53,9 @@ NavigationBarDirective = (currentUserService, navigationBarService, $location) -
NavigationBarDirective.$inject = [
"tgCurrentUserService",
- "tgNavigationBarService"
- "$location"
+ "tgNavigationBarService",
+ "$tgLocation",
+ "$tgNavUrls"
]
angular.module("taigaNavigationBar").directive("tgNavigationBar", NavigationBarDirective)
diff --git a/app/modules/navigation-bar/navigation-bar.directive.spec.coffee b/app/modules/navigation-bar/navigation-bar.directive.spec.coffee
index 7c3251bc..36041e2f 100644
--- a/app/modules/navigation-bar/navigation-bar.directive.spec.coffee
+++ b/app/modules/navigation-bar/navigation-bar.directive.spec.coffee
@@ -41,6 +41,19 @@ describe "navigationBarDirective", () ->
provide.value "tgCurrentUserService", mocks.currentUserService
+ _mocksLocationService = () ->
+ mocks.locationService = {
+ url: sinon.stub()
+ search: sinon.stub()
+ }
+
+ provide.value "$tgLocation", mocks.locationService
+
+ _mockTgNavUrls = () ->
+ mocks.navUrls = {
+ resolve: sinon.stub()
+ }
+ provide.value "$tgNavUrls", mocks.navUrls
_mockTranslateFilter = () ->
mockTranslateFilter = (value) ->
@@ -58,6 +71,8 @@ describe "navigationBarDirective", () ->
provide = $provide
_mocksCurrentUserService()
+ _mocksLocationService()
+ _mockTgNavUrls( )
_mockTranslateFilter()
_mockTgDropdownProjectListDirective()
_mockTgDropdownUserDirective()
@@ -90,3 +105,33 @@ describe "navigationBarDirective", () ->
mocks.currentUserService.isAuthenticated.returns(true)
expect(elm.isolateScope().vm.isAuthenticated).to.be.true
+
+ it "navigation bar login", () ->
+ mocks.navUrls.resolve.withArgs("login").returns("/login")
+ nextUrl = "/discover/search?order_by=-total_activity_last_month"
+ mocks.locationService.url.returns(nextUrl)
+ elm = createDirective()
+ scope.$apply()
+ vm = elm.isolateScope().vm
+ expect(mocks.locationService.url.callCount).to.be.equal(0)
+ expect(mocks.locationService.search.callCount).to.be.equal(0)
+ vm.login()
+ expect(mocks.locationService.url.callCount).to.be.equal(2)
+ expect(mocks.locationService.search.callCount).to.be.equal(1)
+ expect(mocks.locationService.url.calledWith("/login")).to.be.true
+ expect(mocks.locationService.search.calledWith({next: encodeURIComponent(nextUrl)})).to.be.true
+
+ it "navigation bar register", () ->
+ mocks.navUrls.resolve.withArgs("register").returns("/register")
+ nextUrl = "/discover/search?order_by=-total_activity_last_month"
+ mocks.locationService.url.returns(nextUrl)
+ elm = createDirective()
+ scope.$apply()
+ vm = elm.isolateScope().vm
+ expect(mocks.locationService.url.callCount).to.be.equal(0)
+ expect(mocks.locationService.search.callCount).to.be.equal(0)
+ vm.register()
+ expect(mocks.locationService.url.callCount).to.be.equal(2)
+ expect(mocks.locationService.search.callCount).to.be.equal(1)
+ expect(mocks.locationService.url.calledWith("/register")).to.be.true
+ expect(mocks.locationService.search.calledWith({next: encodeURIComponent(nextUrl)})).to.be.true
diff --git a/app/modules/navigation-bar/navigation-bar.jade b/app/modules/navigation-bar/navigation-bar.jade
index 54aa167e..83f49a36 100644
--- a/app/modules/navigation-bar/navigation-bar.jade
+++ b/app/modules/navigation-bar/navigation-bar.jade
@@ -3,7 +3,7 @@ nav.navbar(ng-if="vm.isEnabledHeader")
a.logo(
href="#",
tg-nav="home",
- title="{{'PROJECT.NAVIGATION.DASHBOARD_TITLE' | translate}}")
+ title="{{'PROJECT.NAVIGATION.HOMEPAGE' | translate}}")
include ../../svg/logo.svg
@@ -15,16 +15,16 @@ nav.navbar(ng-if="vm.isEnabledHeader")
div.nav-right(ng-if="!vm.isAuthenticated")
a.login(
- tg-nav="login",
+ ng-click="vm.login()"
href="#",
- title="{{ 'LOGIN_COMMON.ACTION_SIGN_IN' | translate }}"
+ title="{{ 'LOGIN_COMMON.ACTION_SIGN_IN' | translate }}"
) {{ 'LOGIN_COMMON.ACTION_SIGN_IN' | translate }}
a.register(
- tg-nav="register",
+ ng-click="vm.register()"
href="#",
- title="{{ 'REGISTER_FORM.ACTION_SIGN_UP' | translate }}"
+ title="{{ 'REGISTER_FORM.ACTION_SIGN_UP' | translate }}"
) {{ 'REGISTER_FORM.ACTION_SIGN_UP' | translate }}
-
+
div.nav-right(ng-if="vm.isAuthenticated")
a(tg-nav="home",
ng-class="{active: vm.active}",
@@ -32,6 +32,13 @@ nav.navbar(ng-if="vm.isEnabledHeader")
include ../../svg/dashboard.svg
+ a(
+ href="#",
+ tg-nav="discover",
+ title="{{'PROJECT.NAVIGATION.DISCOVER_TITLE' | translate}}",
+ )
+ include ../../svg/discover.svg
+
div.topnav-dropdown-wrapper(ng-show="vm.projects.size", tg-dropdown-project-list)
- //div.topnav-dropdown-wrapper(tg-dropdown-organization-list)
+ //- div.topnav-dropdown-wrapper(tg-dropdown-organization-list)
div.topnav-dropdown-wrapper(tg-dropdown-user)
diff --git a/app/modules/profile/profile-favs/items/project.jade b/app/modules/profile/profile-favs/items/project.jade
index 2cb121d9..827eff4f 100644
--- a/app/modules/profile/profile-favs/items/project.jade
+++ b/app/modules/profile/profile-favs/items/project.jade
@@ -1,13 +1,26 @@
.list-itemtype-project
- .list-itemtype-project-data
- h2
- a(
+ .list-itemtype-project-left
+ .list-itemtype-project-data-wrapper
+
+ a.list-itemtype-project-image(
href="#"
tg-nav="project:project=vm.item.get('slug')"
title="{{ ::vm.item.get('name') }}"
- ) {{ ::vm.item.get('name') }}
- span.private(ng-if="::project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
- p {{ ::vm.item.get('description') }}
+ )
+ img(
+ tg-project-logo-src="vm.item"
+ title="{{ ::vm.item.get('name') }}"
+ )
+
+ .list-itemtype-project-data
+ h2
+ a(
+ href="#"
+ tg-nav="project:project=vm.item.get('slug')"
+ title="{{ ::vm.item.get('name') }}"
+ ) {{ ::vm.item.get('name') }}
+ span.private(ng-if="::project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
+ p {{ ::vm.item.get('description') }}
.list-itemtype-project-tags.tags-container(ng-if="::vm.item.get('tags_colors').size")
span.tag(
diff --git a/app/modules/profile/profile-projects/profile-projects.jade b/app/modules/profile/profile-projects/profile-projects.jade
index 3d4e8b2c..28c2ef45 100644
--- a/app/modules/profile/profile-projects/profile-projects.jade
+++ b/app/modules/profile/profile-projects/profile-projects.jade
@@ -13,15 +13,24 @@ section.profile-projects
.list-itemtype-project(tg-repeat="project in vm.projects")
.list-itemtype-project-left
-
- .project-list-single-title
- h2
- a(
- href="#"
- tg-nav="project:project=project.get('slug')"
- title="{{ ::project.get('name') }}"
- ) {{::project.get('name')}}
- p {{ ::project.get('description') | limitTo:300 }}
+ .project-list-single-title-wrapper
+ a.list-itemtype-project-image(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{ ::project.get('name') }}"
+ )
+ img(
+ tg-project-logo-src="::project"
+ alt="{{::project.get('name')}}"
+ )
+ .project-list-single-title
+ h2
+ a(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{ ::project.get('name') }}"
+ ) {{::project.get('name')}}
+ p {{ ::project.get('description') | limitTo:300 }}
.list-itemtype-project-tags.tags-container(ng-if="::project.get('tags').size")
span.tag(
diff --git a/app/modules/profile/styles/profile-content-tabs.scss b/app/modules/profile/styles/profile-content-tabs.scss
index 492778cc..6b5340be 100644
--- a/app/modules/profile/styles/profile-content-tabs.scss
+++ b/app/modules/profile/styles/profile-content-tabs.scss
@@ -4,7 +4,7 @@
.tab {
color: $gray-light;
display: inline-block;
- padding: 1rem 1.25rem;
+ padding: 1rem;
&:hover,
&.active {
color: $grayer;
diff --git a/app/modules/projects/listing/projects-listing.jade b/app/modules/projects/listing/projects-listing.jade
index 60dab648..3e1e0de9 100644
--- a/app/modules/projects/listing/projects-listing.jade
+++ b/app/modules/projects/listing/projects-listing.jade
@@ -1,28 +1,60 @@
-div.project-list-wrapper.centered
- div.project-list-title
+.project-list-wrapper.centered
+ .project-list-title
h1(translate="PROJECTS.MY_PROJECTS")
- div.create-options
- a.create-project-btn.button-green(href="#", ng-click="vm.newProject()", title="{{'PROJECT.NAVIGATION.ACTION_CREATE_PROJECT' | translate}}", translate="PROJECT.NAVIGATION.ACTION_CREATE_PROJECT")
+ .create-options
+ a.create-project-btn.button-green(
+ href="#"
+ ng-click="vm.newProject()"
+ title="{{'PROJECT.NAVIGATION.ACTION_CREATE_PROJECT' | translate}}"
+ translate="PROJECT.NAVIGATION.ACTION_CREATE_PROJECT"
+ )
span(tg-import-project-button)
- a.button-blackish.import-project-button(href="", title="{{'PROJECT.NAVIGATION.TITLE_IMPORT_PROJECT' | translate}}")
+ a.button-blackish.import-project-button(
+ href=""
+ title="{{'PROJECT.NAVIGATION.TITLE_IMPORT_PROJECT' | translate}}"
+ )
span.icon.icon-upload
input.import-file.hidden(type="file")
section.project-list-section
- div.project-list
+ .project-list
ul(tg-sort-projects="vm.projects")
- li.list-itemtype-project(tg-bind-scope, tg-repeat="project in vm.projects track by project.get('id')")
- div.list-itemtype-project-left
- div.list-itemtype-project-data
- h2
- a(href="#", tg-nav="project:project=project.get('slug')", title="{{ ::project.get('name') }}") {{project.get('name')}}
- span.private(ng-if="project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}")
- include ../../../svg/lock.svg
- p {{ ::project.get('description') | limitTo:300 }}
- span(ng-if="::project.get('description').length > 300") ...
-
- div.list-itemtype-project-tags.tag-container(ng-if="::project.get('tags').size")
- span.tag(style='border-left: 5px solid {{::tag.get("color")}};', tg-repeat="tag in ::project.get('colorized_tags')")
+ li.list-itemtype-project(
+ tg-bind-scope
+ tg-repeat="project in vm.projects track by project.get('id')"
+ )
+ .list-itemtype-project-left
+
+ .list-itemtype-project-data-wrapper
+ a.list-itemtype-project-image(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{ ::project.get('name') }}"
+ )
+ img(
+ tg-project-logo-src="::project"
+ alt="{{::project.get('name')}}"
+ )
+ .list-itemtype-project-data
+ h2
+ a(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{ ::project.get('name') }}"
+ ) {{project.get('name')}}
+ span.private(
+ ng-if="project.get('is_private')"
+ title="{{'PROJECT.PRIVATE' | translate}}"
+ )
+ include ../../../svg/lock.svg
+ p {{ ::project.get('description') | limitTo:300 }}
+ span(ng-if="::project.get('description').length > 300") ...
+
+ .list-itemtype-project-tags.tag-container(ng-if="::project.get('tags').size")
+ span.tag(
+ style='border-left: 5px solid {{::tag.get("color")}};'
+ tg-repeat="tag in ::project.get('colorized_tags')"
+ )
span.tag-name {{::tag.get('name')}}
span.drag.icon.icon-drag-v
diff --git a/app/modules/projects/listing/styles/profile-projects.scss b/app/modules/projects/listing/styles/profile-projects.scss
index 534e9738..e068e7c7 100644
--- a/app/modules/projects/listing/styles/profile-projects.scss
+++ b/app/modules/projects/listing/styles/profile-projects.scss
@@ -4,6 +4,12 @@
display: flex;
justify-content: space-between;
min-height: 10rem;
+ .project-list-single-title-wrapper {
+ display: flex;
+ }
+ .list-itemtype-project-image {
+ flex-shrink: 0;
+ }
.list-itemtype-project-right {
display: flex;
flex-direction: column;
diff --git a/app/modules/projects/listing/styles/project-list.scss b/app/modules/projects/listing/styles/project-list.scss
index daf97513..b9f68b93 100644
--- a/app/modules/projects/listing/styles/project-list.scss
+++ b/app/modules/projects/listing/styles/project-list.scss
@@ -54,6 +54,13 @@
opacity: 1;
}
}
+ .list-itemtype-project-data-wrapper {
+ display: flex;
+ }
+ .list-itemtype-project-image {
+ flex-shrink: 0;
+ margin-right: 1rem;
+ }
}
.drag {
@extend %large;
diff --git a/app/modules/projects/project/project.jade b/app/modules/projects/project/project.jade
index 8977c920..b646fbff 100644
--- a/app/modules/projects/project/project.jade
+++ b/app/modules/projects/project/project.jade
@@ -2,51 +2,67 @@ div.wrapper
tg-project-menu
div.single-project.centered
section.single-project-intro
- div.intro-options
- h1
- span.project-name {{::vm.project.get("name")}}
- span.private(
- ng-if="::vm.project.get('is_private')"
- title="{{'PROJECT.PRIVATE' | translate}}"
- )
- include ../../../svg/lock.svg
-
- //- Like and wacht buttons for authenticated users
- div.track-buttons-container(ng-if="vm.user")
- tg-like-project-button(project="vm.project")
- tg-watch-project-button(project="vm.project")
-
- //- Like and wacht buttons for anonymous users
- div.track-container(ng-if="!vm.user")
- .list-itemtype-track
- span.list-itemtype-track-likers(
- title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_fans\")||0}:'messageformat' }}"
- )
- span.icon
- include ../../../svg/like.svg
- span {{ ::vm.project.get('total_fans') }}
-
- span.list-itemtype-track-watchers(
- title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_watchers\")||0}:'messageformat' }}"
- )
- span.icon
- include ../../../svg/watch.svg
- span {{ ::vm.project.get('total_watchers') }}
-
- p.description {{vm.project.get('description')}}
-
- div.single-project-tags.tags-container(ng-if="::vm.project.get('tags').size")
- span.tag(
- style='border-left: 5px solid {{::tag.get("color")}};',
- tg-repeat="tag in ::vm.project.get('colorized_tags')"
+ .project-logo(
+ href="#"
+ tg-nav="project:project=project.get('slug')"
+ title="{{::project.get('name')}}"
+ )
+ img(
+ tg-project-logo-src="vm.project"
+ alt="{{::vm.project.get('name')}}"
)
- span.tag-name {{::tag.get('name')}}
+ .single-project-title-wrapper
+ .intro-options
+ .intro-title
+ h1
+ span.project-name {{::vm.project.get("name")}}
+ span.private(
+ ng-if="::vm.project.get('is_private')"
+ title="{{'PROJECT.PRIVATE' | translate}}"
+ )
+ include ../../../svg/lock.svg
+
+ div.track-buttons-container(ng-if="vm.user")
+ tg-like-project-button(project="vm.project")
+ tg-watch-project-button(project="vm.project")
+
+ div.track-container(ng-if="!vm.user")
+ .list-itemtype-track
+ span.list-itemtype-track-likers(
+ title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_fans\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../svg/like.svg
+ span {{ ::vm.project.get('total_fans') }}
+
+ span.list-itemtype-track-watchers(
+ title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_watchers\")||0}:'messageformat' }}"
+ )
+ span.icon
+ include ../../../svg/watch.svg
+ span {{ ::vm.project.get('total_watchers') }}
+
+ p.description {{vm.project.get('description')}}
+
+ div.single-project-tags.tags-container(ng-if="::vm.project.get('tags').size")
+ span.tag(
+ style='border-left: 5px solid {{::tag.get("color")}};',
+ tg-repeat="tag in ::vm.project.get('colorized_tags')"
+ )
+ span.tag-name {{::tag.get('name')}}
div.project-data
section.timeline(ng-if="vm.project")
div(tg-user-timeline, projectId="vm.project.get('id')")
section.involved-data
+ .looking-for-people(ng-if="vm.project.get('is_looking_for_people')")
+ img(
+ src="/#{v}/images/looking-for-people.png"
+ title="{{'PROJECT.LOOKING_FOR_PEOPLE' | translate}}"
+ )
+ h3 {{'PROJECT.LOOKING_FOR_PEOPLE' | translate}}
+ p(ng-if="vm.project.get('looking_for_people_note')") {{::vm.project.get('looking_for_people_note')}}"
h2.title {{"PROJECT.SECTION.TEAM" | translate}}
ul.involved-team
li(tg-repeat="member in vm.members")
@@ -55,11 +71,3 @@ div.wrapper
title="{{::member.get('full_name')}}"
)
img(ng-src="{{::member.get('photo')}}", alt="{{::member.get('full_name')}}")
- //-
- h2.title Organizations
- div.involved-organization
- a(href="", title="User Name")
- img(
- src="https://s3.amazonaws.com/uifaces/faces/twitter/dan_higham/48.jpg"
- alt="{{member.full_name}}"
- )
diff --git a/app/modules/resources/projects-resource.service.coffee b/app/modules/resources/projects-resource.service.coffee
index 35de7d51..4385b90c 100644
--- a/app/modules/resources/projects-resource.service.coffee
+++ b/app/modules/resources/projects-resource.service.coffee
@@ -22,6 +22,20 @@ pagination = () ->
Resource = (urlsService, http, paginateResponseService) ->
service = {}
+ service.getProjects = (params = {}, pagination = true) ->
+ url = urlsService.resolve("projects")
+
+ httpOptions = {}
+
+ if !pagination
+ httpOptions = {
+ headers: {
+ "x-disable-pagination": "1"
+ }
+ }
+
+ return http.get(url, params, httpOptions)
+
service.getProjectBySlug = (projectSlug) ->
url = urlsService.resolve("projects")
diff --git a/app/modules/resources/resources.coffee b/app/modules/resources/resources.coffee
index b6e8a17f..e8a4c33a 100644
--- a/app/modules/resources/resources.coffee
+++ b/app/modules/resources/resources.coffee
@@ -25,7 +25,8 @@ services = [
"tgTasksResource",
"tgIssuesResource",
"tgExternalAppsResource",
- "tgAttachmentsResource"
+ "tgAttachmentsResource",
+ "tgStatsResource"
]
Resources = ($injector) ->
diff --git a/app/modules/resources/stats-resource.service.coffee b/app/modules/resources/stats-resource.service.coffee
new file mode 100644
index 00000000..2376ae4c
--- /dev/null
+++ b/app/modules/resources/stats-resource.service.coffee
@@ -0,0 +1,34 @@
+###
+# Copyright (C) 2014-2015 Taiga Agile LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+# File: stats-resource.service.coffee
+###
+
+Resource = (urlsService, http) ->
+ service = {}
+
+ service.discover = (applicationId, state) ->
+ url = urlsService.resolve("stats-discover")
+ return http.get(url).then (result) ->
+ Immutable.fromJS(result.data)
+
+ return () ->
+ return {"stats": service}
+
+Resource.$inject = ["$tgUrls", "$tgHttp"]
+
+module = angular.module("taigaResources2")
+module.factory("tgStatsResource", Resource)
diff --git a/app/partials/admin/admin-project-profile.jade b/app/partials/admin/admin-project-profile.jade
index aebc5d45..235202c9 100644
--- a/app/partials/admin/admin-project-profile.jade
+++ b/app/partials/admin/admin-project-profile.jade
@@ -1,7 +1,10 @@
doctype html
-div.wrapper(tg-project-profile, ng-controller="ProjectProfileController as ctrl",
- ng-init="section='admin'; sectionName='ADMIN.PROJECT_PROFILE.PROJECT_DETAILS'")
+div.wrapper(
+ tg-project-profile
+ ng-controller="ProjectProfileController as ctrl"
+ ng-init="section='admin'; sectionName='ADMIN.PROJECT_PROFILE.PROJECT_DETAILS'"
+)
tg-project-menu
sidebar.menu-secondary.sidebar.settings-nav(tg-admin-navigation="project-profile")
@@ -15,44 +18,137 @@ div.wrapper(tg-project-profile, ng-controller="ProjectProfileController as ctrl"
include ../includes/components/mainTitle
form
- fieldset
- label(for="project-name", translate="ADMIN.PROJECT_PROFILE.PROJECT_NAME")
- input(type="text", name="name", placeholder="{{'ADMIN.PROJECT_PROFILE.PROJECT_NAME' | translate}}", id="project-name",
- ng-model="project.name", data-required="true", maxlength="45")
- fieldset
- label(for="project-sprints", translate="ADMIN.PROJECT_PROFILE.NUMBER_SPRINTS")
- input(type="number", name="total_milestones", min="0", placeholder="{{'ADMIN.PROJECT_PROFILE.NUMBER_SPRINTS' | translate}}",
- id="project-sprints", ng-model="project.total_milestones", data-type="digits")
+ .project-details-image(tg-project-logo)
+ fieldset.image-container
+ img.image(
+ tg-project-logo-src="project._attrs"
+ alt="logo"
+ )
+ .loading-overlay
+ img.loading-spinner(
+ src="/#{v}/svg/spinner-circle.svg",
+ alt="{{'COMMON.LOADING' | translate}}"
+ )
+ input.hidden(
+ type="file"
+ id="logo-field"
+ tg-project-logo-model="logoAttachment"
+ )
+ p.image-help
+ span {{ 'ADMIN.PROJECT_PROFILE.LOGO_HELP' | translate }}
+ span.size-info.hidden(tg-bo-html="maxFileSizeMsg")
- fieldset
- label(for="total-story-points", translate="ADMIN.PROJECT_PROFILE.NUMBER_US_POINTS")
- input(type="number", name="total_story_points", min="0", placeholder="{{'ADMIN.PROJECT_PROFILE.NUMBER_US_POINTS' | translate}}",
- id="total-story-points", ng-model="project.total_story_points",
- data-type="digits")
+ a.button-green.change.js-change-logo(
+ href="#"
+ title="{{'ADMIN.PROJECT_PROFILE.CHANGE_LOGO' | translate}}"
+ ) {{'ADMIN.PROJECT_PROFILE.CHANGE_LOGO' | translate}}
- fieldset
- label(for="tags", translate="ADMIN.PROJECT_PROFILE.TAGS")
- div.tags-block(ng-if="project.id", tg-lb-tag-line, ng-model="project.tags")
+ a.use-default-image.js-use-default-logo(
+ href="#"
+ title="{{ 'ADMIN.PROJECT_PROFILE.ACTION_USE_DEFAULT_LOGO' | translate }}"
+ ) {{ 'ADMIN.PROJECT_PROFILE.ACTION_USE_DEFAULT_LOGO' | translate }}
- fieldset
- label(for="project-description", translate="ADMIN.PROJECT_PROFILE.DESCRIPTION")
- textarea(name="description", ng-attr-placeholder="{{'ADMIN.PROJECT_PROFILE.DESCRIPTION' | translate}}", id="project-description",
- ng-model="project.description", data-required="true")
- div
- div.privacy-settings
- div
- input.privacy-project(type="radio", name="private-project", ng-model="project.is_private", ng-value="false")
- label.trans-button(for="public-project")
- span(translate="ADMIN.PROJECT_PROFILE.PUBLIC_PROJECT")
- div
- input.privacy-project(type="radio", name="private-project", ng-model="project.is_private", ng-value="true")
- label.trans-button(for="private-project")
- span(translate="ADMIN.PROJECT_PROFILE.PRIVATE_PROJECT")
+ .project-details-form-data
- button.button-green.submit-button(type="submit", title="{{'COMMON.SAVE' | translate}}", translate="COMMON.SAVE")
- a.delete-project(href="", title="{{'ADMIN.PROJECT_PROFILE.DELETE' | translate}}", ng-click="ctrl.openDeleteLightbox()", translate="ADMIN.PROJECT_PROFILE.DELETE")
+ fieldset
+ label(for="project-name") {{ 'ADMIN.PROJECT_PROFILE.PROJECT_NAME' | translate }}
+ input(
+ type="text"
+ name="name"
+ placeholder="{{'ADMIN.PROJECT_PROFILE.PROJECT_NAME' | translate}}"
+ id="project-name"
+ ng-model="project.name"
+ data-required="true"
+ maxlength="45"
+ )
+
+ fieldset
+ label(for="project-description") {{ 'ADMIN.PROJECT_PROFILE.DESCRIPTION' | translate }}
+ textarea(
+ name="description"
+ ng-attr-placeholder="{{'ADMIN.PROJECT_PROFILE.DESCRIPTION' | translate}}"
+ id="project-description"
+ ng-model="project.description"
+ data-required="true"
+ )
+
+ fieldset
+ label(for="tags") {{ 'ADMIN.PROJECT_PROFILE.TAGS' | translate }}
+ div.tags-block(
+ ng-if="project.id"
+ tg-lb-tag-line
+ ng-model="project.tags"
+ )
+ fieldset.looking-for-people
+ .looking-for-people-selector
+ span {{ 'ADMIN.PROJECT_PROFILE.RECRUITING' | translate }}
+ span(
+ title="{{ 'ADMIN.PROJECT_PROFILE.RECRUITING_MESSAGE' | translate }}"
+ )
+ include ../../svg/recruit.svg
+ div.check
+ input(
+ type="checkbox",
+ ng-model="project.is_looking_for_people"
+ )
+ div
+ span.check-text.check-yes(translate="COMMON.YES")
+ span.check-text.check-no(translate="COMMON.NO")
+
+ .looking-for-people-reason(ng-show="project.is_looking_for_people")
+ label {{ 'ADMIN.PROJECT_PROFILE.RECRUITING_MESSAGE' | translate }}
+ input(
+ type="text"
+ maxlength="200"
+ ng-model="project.looking_for_people_note"
+ placeholder="{{ 'ADMIN.PROJECT_PROFILE.RECRUITING_PLACEHOLDER' | translate }}"
+ )
+
+ fieldset
+ .project-privacy-settings
+ div.privacy-option
+ input.privacy-project(
+ type="radio"
+ id="private-project"
+ name="privacy-project"
+ ng-model="project.is_private"
+ ng-value="false"
+ )
+ label.trans-button(for="private-project") {{ 'ADMIN.PROJECT_PROFILE.PUBLIC_PROJECT' | translate }}
+ span(title="{{ 'ADMIN.PROJECT_PROFILE.PUBLIC_PROJECT_DESC' | translate }}")
+ include ../../svg/help.svg
+
+ div.privacy-option
+ input.privacy-project(
+ type="radio"
+ id="public-project"
+ name="privacy-project"
+ ng-model="project.is_private"
+ ng-value="true"
+ )
+ label.trans-button(for="public-project") {{'ADMIN.PROJECT_PROFILE.PRIVATE_PROJECT' | translate }}
+ span(title="{{ 'ADMIN.PROJECT_PROFILE.PRIVATE_PROJECT_DESC' | translate }}")
+ include ../../svg/help.svg
+
+ a.private-or-public(
+ href="https://taiga.io/support/whats-the-difference-between-public-and-private-projects/"
+ target="_blank"
+ )
+ span(title="{{ 'ADMIN.PROJECT_PROFILE.PRIVATE_OR_PUBLIC' | translate }}")
+ include ../../svg/help.svg
+ span {{'ADMIN.PROJECT_PROFILE.PRIVATE_OR_PUBLIC' | translate }}
+ button.button-green.submit-button(
+ type="submit"
+ title="{{'COMMON.SAVE' | translate}}"
+ translate="COMMON.SAVE"
+ )
+ a.delete-project(
+ href=""
+ title="{{'ADMIN.PROJECT_PROFILE.DELETE' | translate}}"
+ ng-click="ctrl.openDeleteLightbox()"
+ ) {{ 'ADMIN.PROJECT_PROFILE.DELETE' | translate }}
div.lightbox.lightbox-delete-project(tg-lb-delete-project)
include ../includes/modules/lightbox-delete-project
diff --git a/app/partials/includes/components/mainTitle.jade b/app/partials/includes/components/mainTitle.jade
index 8e7fa75d..24914a14 100644
--- a/app/partials/includes/components/mainTitle.jade
+++ b/app/partials/includes/components/mainTitle.jade
@@ -1,2 +1,6 @@
header
- h1(tg-main-title, project-name="project.name", i18n-section-name="{{ sectionName }}")
+ h1(
+ tg-main-title
+ project-name="project.name"
+ i18n-section-name="{{ sectionName }}"
+ )
diff --git a/app/partials/user/mail-notifications.jade b/app/partials/user/mail-notifications.jade
index 35485362..2f4a5986 100644
--- a/app/partials/user/mail-notifications.jade
+++ b/app/partials/user/mail-notifications.jade
@@ -1,8 +1,10 @@
doctype html
-div.wrapper(tg-user-notifications, ng-controller="UserNotificationsController as ctrl",
- ng-init="section='mail-notifications'")
- tg-project-menu
+div.wrapper(
+ tg-user-notifications
+ ng-controller="UserNotificationsController as ctrl",
+ ng-init="section='mail-notifications'"
+)
sidebar.menu-secondary.sidebar.settings-nav(tg-user-settings-navigation="mail-notifications")
include ../includes/modules/user-settings-menu
diff --git a/app/partials/user/user-change-password.jade b/app/partials/user/user-change-password.jade
index 734005c9..5b5e0b5d 100644
--- a/app/partials/user/user-change-password.jade
+++ b/app/partials/user/user-change-password.jade
@@ -5,8 +5,6 @@ div.wrapper(
ng-controller="UserChangePasswordController as ctrl"
ng-init="section='user-settings'"
)
- tg-project-menu
-
sidebar.menu-secondary.sidebar.settings-nav(tg-user-settings-navigation="change-password")
include ../includes/modules/user-settings-menu
diff --git a/app/partials/user/user-profile.jade b/app/partials/user/user-profile.jade
index 7064ba34..2f4c0470 100644
--- a/app/partials/user/user-profile.jade
+++ b/app/partials/user/user-profile.jade
@@ -1,126 +1,129 @@
doctype html
-div.wrapper(tg-user-profile, ng-controller="UserSettingsController as ctrl",
- ng-init="section='user-settings'")
+div.wrapper(
+ tg-user-profile
+ ng-controller="UserSettingsController as ctrl"
+ ng-init="section='user-settings'"
+)
sidebar.menu-secondary.sidebar.settings-nav(tg-user-settings-navigation="user-profile")
include ../includes/modules/user-settings-menu
section.main.user-profile
header
- h1
- span.green {{sectionName | translate}}
+ include ../includes/components/mainTitle
form
- div.container
- div.avatar-container
- fieldset(tg-user-avatar)
- .image-container
- img.avatar(ng-src="{{user.big_photo}}" alt="avatar")
- .overlay.hidden
- img.loading-spinner(
- src="/#{v}/svg/spinner-circle.svg",
- alt="{{'COMMON.LOADING' | translate}}"
- )
-
- input.hidden(
- type="file"
- id="avatar-field"
- tg-avatar-model="avatarAttachment"
+ .project-details-image(tg-user-avatar)
+ fieldset.image-container
+ img.image(ng-src="{{user.big_photo}}" alt="avatar")
+ .loading-overlay
+ img.loading-spinner(
+ src="/#{v}/svg/spinner-circle.svg",
+ alt="{{'COMMON.LOADING' | translate}}"
)
+ input.hidden(
+ type="file"
+ id="avatar-field"
+ tg-avatar-model="avatarAttachment"
+ )
+ p.image-help
+ span {{ 'USER_PROFILE.IMAGE_HELP' | translate }}
+ span.size-info.hidden(tg-bo-html="maxFileSizeMsg")
- p(translate="USER_PROFILE.IMAGE_HELP")
- span.size-info.hidden(tg-bo-html="maxFileSizeMsg")
+ a.button-green.change.js-change-avatar(
+ href="#"
+ title="{{'USER_PROFILE.CHANGE_PHOTO' | translate}}"
+ ) {{'USER_PROFILE.CHANGE_PHOTO' | translate}}
- a.button-green.change.js-change-avatar(
- translate="USER_PROFILE.ACTION_CHANGE_IMAGE",
- title="{{'USER_PROFILE.CHANGE_PHOTO' | translate}} {{maxFileSizeMsg}}"
- )
- a.use-gravatar(translate="USER_PROFILE.ACTION_USE_GRAVATAR")
+ a.use-default-image.js-use-gravatar(
+ href="#"
+ title="{{ 'USER_PROFILE.ACTION_USE_GRAVATAR' | translate }}"
+ ) {{ 'USER_PROFILE.ACTION_USE_GRAVATAR' | translate }}
- div.data
- fieldset
- label(for="username", translate="USER_PROFILE.FIELD.USERNAME")
- input(
- type="text"
- autocorrect="off"
- autocapitalize="none"
- name="username"
- id="username"
- ng-model="user.username"
- data-required="true"
- data-maxlength="255"
- data-regexp="^[\\w.-]+$"
- placeholder="{{'USER_PROFILE.FIELD.USERNAME' | translate}}",
- )
+ .project-details-form-data
+ fieldset
+ label(for="username", translate="USER_PROFILE.FIELD.USERNAME")
+ input(
+ type="text"
+ autocorrect="off"
+ autocapitalize="none"
+ name="username"
+ id="username"
+ ng-model="user.username"
+ data-required="true"
+ data-maxlength="255"
+ data-regexp="^[\\w.-]+$"
+ placeholder="{{'USER_PROFILE.FIELD.USERNAME' | translate}}",
+ )
- fieldset
- label(for="email", translate="USER_PROFILE.FIELD.EMAIL")
- input(
- type="email"
- name="email"
- id="email"
- ng-model="user.email"
- data-type="email"
- data-required="true"
- data-maxlength="255"
- placeholder="{{'USER_PROFILE.FIELD.EMAIL' | translate}}"
- )
+ fieldset
+ label(for="email", translate="USER_PROFILE.FIELD.EMAIL")
+ input(
+ type="email"
+ name="email"
+ id="email"
+ ng-model="user.email"
+ data-type="email"
+ data-required="true"
+ data-maxlength="255"
+ placeholder="{{'USER_PROFILE.FIELD.EMAIL' | translate}}"
+ )
- fieldset
- label(for="full-name", translate="USER_PROFILE.FIELD.FULL_NAME")
- input(
- type="text"
- name="full_name"
- id="full-name"
- ng-model="user.full_name"
- data-required="true"
- data-maxlength="256"
- placeholder="{{'USER_PROFILE.FIELD.PLACEHOLDER_FULL_NAME' | translate}}",
- )
+ fieldset
+ label(for="full-name", translate="USER_PROFILE.FIELD.FULL_NAME")
+ input(
+ type="text"
+ name="full_name"
+ id="full-name"
+ ng-model="user.full_name"
+ data-required="true"
+ data-maxlength="256"
+ placeholder="{{'USER_PROFILE.FIELD.PLACEHOLDER_FULL_NAME' | translate}}",
+ )
- fieldset
- label(for="lang", translate="USER_PROFILE.FIELD.LANGUAGE")
- select(
- name="lang"
- id="lang"
- ng-model="lang"
- ng-options="locale.code as locale.name for locale in locales"
- )
- option(value="", translate="USER_PROFILE.FIELD.LANGUAGE_DEFAULT")
+ fieldset
+ label(for="lang", translate="USER_PROFILE.FIELD.LANGUAGE")
+ select(
+ name="lang"
+ id="lang"
+ ng-model="lang"
+ ng-options="locale.code as locale.name for locale in locales"
+ )
+ option(value="", translate="USER_PROFILE.FIELD.LANGUAGE_DEFAULT")
- fieldset
- label(for="theme", translate="USER_PROFILE.FIELD.THEME")
- select(
- name="theme"
- id="theme"
- ng-model="theme"
- ng-options="availableTheme for availableTheme in availableThemes"
- )
- option(value="", translate="USER_PROFILE.FIELD.THEME_DEFAULT")
+ fieldset
+ label(for="theme", translate="USER_PROFILE.FIELD.THEME")
+ select(
+ name="theme"
+ id="theme"
+ ng-model="theme"
+ ng-options="availableTheme for availableTheme in availableThemes"
+ )
+ option(value="", translate="USER_PROFILE.FIELD.THEME_DEFAULT")
- fieldset
- label(for="bio", translate="USER_PROFILE.FIELD.BIO")
- textarea(
- name="bio"
- id="bio"
- ng-model="user.bio"
- ng-attr-placeholder="{{'USER_PROFILE.FIELD.PLACEHOLDER_BIO' | translate}}"
- ng-maxlength="210"
- maxlength="210"
- )
+ fieldset
+ label(for="bio", translate="USER_PROFILE.FIELD.BIO")
+ textarea(
+ name="bio"
+ id="bio"
+ ng-model="user.bio"
+ ng-attr-placeholder="{{'USER_PROFILE.FIELD.PLACEHOLDER_BIO' | translate}}"
+ ng-maxlength="210"
+ maxlength="210"
+ )
- fieldset.submit
- button.button-green.submit-button(
- type="submit"
- title="{{'COMMON.SAVE' | translate}}",
- translate="COMMON.SAVE"
- )
- a.delete-account(
- href=""
- title="{{'USER_PROFILE.ACTION_DELETE_ACCOUNT' | translate}}"
- ng-click="ctrl.openDeleteLightbox()"
- translate="USER_PROFILE.ACTION_DELETE_ACCOUNT"
- )
+ fieldset.submit
+ button.button-green.submit-button(
+ type="submit"
+ title="{{'COMMON.SAVE' | translate}}",
+ translate="COMMON.SAVE"
+ )
+ a.delete-account(
+ href=""
+ title="{{'USER_PROFILE.ACTION_DELETE_ACCOUNT' | translate}}"
+ ng-click="ctrl.openDeleteLightbox()"
+ translate="USER_PROFILE.ACTION_DELETE_ACCOUNT"
+ )
div.lightbox.lightbox-delete-account(tg-lb-delete-user)
diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss
index 30817476..e4f13781 100755
--- a/app/styles/components/buttons.scss
+++ b/app/styles/components/buttons.scss
@@ -4,6 +4,7 @@
@extend %small;
background: transparent;
border: 0;
+ border-radius: 3px;
color: $white;
cursor: pointer;
display: inline-block;
diff --git a/app/styles/components/list-items.scss b/app/styles/components/list-items.scss
index b042c028..0b715bac 100644
--- a/app/styles/components/list-items.scss
+++ b/app/styles/components/list-items.scss
@@ -35,6 +35,17 @@
h2 {
@extend %large;
}
+ .list-itemtype-project-data-wrapper {
+ display: flex;
+ }
+ .list-itemtype-project-image {
+ flex-shrink: 0;
+ margin-right: .5rem;
+ width: 3rem;
+ img {
+ width: 100%;
+ }
+ }
.list-itemtype-project-members {
align-self: flex-end;
display: flex;
diff --git a/app/styles/components/private.scss b/app/styles/components/private.scss
index 00daaaad..943986c3 100644
--- a/app/styles/components/private.scss
+++ b/app/styles/components/private.scss
@@ -3,8 +3,7 @@
margin-left: .5rem;
width: .5rem;
svg {
- height: .5rem;
- width: .5rem;
+ @include svg-size();
}
path {
fill: $gray-light;
diff --git a/app/styles/components/tag.scss b/app/styles/components/tag.scss
index d1fcff0b..235c061f 100644
--- a/app/styles/components/tag.scss
+++ b/app/styles/components/tag.scss
@@ -40,7 +40,10 @@
input {
margin-right: .25rem;
padding: .4rem;
- width: 10rem;
+ width: 14rem;
+ +.icon-floppy {
+ margin-left: .5rem;
+ }
}
.tag {
@extend %small;
diff --git a/app/styles/core/elements.scss b/app/styles/core/elements.scss
index b74468c4..cfd2bbf5 100644
--- a/app/styles/core/elements.scss
+++ b/app/styles/core/elements.scss
@@ -84,3 +84,11 @@ svg {
}
}
}
+
+.spin {
+ img {
+ @extend %loading-spinner;
+ max-height: 2rem;
+ max-width: 2rem;
+ }
+}
diff --git a/app/styles/core/typography.scss b/app/styles/core/typography.scss
index 2b81dda0..f353c83c 100755
--- a/app/styles/core/typography.scss
+++ b/app/styles/core/typography.scss
@@ -36,14 +36,14 @@ h6 {
}
h1 {
- @extend %xxlarge;
- @extend %title;
+ @extend %xlarge;
+ @extend %light;
line-height: 1.5;
margin-bottom: 1rem;
text-transform: uppercase;
span {
- @extend %xxlarge;
+ @extend %larger;
margin-right: .5rem;
overflow: hidden;
text-overflow: ellipsis;
@@ -77,8 +77,8 @@ h1 {
}
h2 {
- @extend %xlarge;
- @extend %title;
+ @extend %larger;
+ @extend %text;
line-height: 1.2;
margin-bottom: 1rem;
}
diff --git a/app/styles/dependencies/mixins.scss b/app/styles/dependencies/mixins.scss
index 2eb9425e..b9101fad 100644
--- a/app/styles/dependencies/mixins.scss
+++ b/app/styles/dependencies/mixins.scss
@@ -109,7 +109,6 @@
@extend %small;
color: $gray-light;
display: flex;
- flex-basis: 150px;
flex-shrink: 0;
justify-content: flex-end;
.list-itemtype-track-likers {
@@ -146,4 +145,24 @@
.in-progress {
cursor: progress;
}
-}
\ No newline at end of file
+}
+
+@mixin centered {
+ margin: 1rem auto;
+ max-width: 1200px;
+ min-width: 768px;
+ @include breakpoint(tablet) {
+ width: 90%;
+ min-width: 0;
+ }
+}
+
+@mixin svg-size($width: 1rem, $height: null) {
+ @if $height == null {
+ width: $width;
+ height: $width;
+ } @else {
+ width: $width;
+ height: $height;
+ }
+}
diff --git a/app/styles/dependencies/mixins/profile-form.scss b/app/styles/dependencies/mixins/profile-form.scss
new file mode 100644
index 00000000..afdbbe2b
--- /dev/null
+++ b/app/styles/dependencies/mixins/profile-form.scss
@@ -0,0 +1,73 @@
+@mixin profile-form {
+ form {
+ display: flex;
+ }
+ fieldset {
+ margin-bottom: 1rem;
+ }
+ label {
+ @extend %light;
+ display: block;
+ margin-bottom: .2rem;
+ }
+ .project-details-image {
+ flex-shrink: 0;
+ flex-grow: 0;
+ width: 180px;
+ margin-right: 2rem;
+ .image {
+ width: 100%;
+ }
+ .loading-spinner {
+
+ }
+ }
+
+ .image-container {
+ position: relative;
+ margin-bottom: 0;
+ }
+ .loading-overlay {
+ display: none;
+ &.active {
+ align-items: center;
+ background: rgba($blackish, .8);
+ bottom: 0;
+ display: flex;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 100%;
+ }
+ }
+ .loading-spinner {
+ @extend %loading-spinner;
+ border: 0;
+ transform-origin: center center;
+ }
+ .image-help {
+ @extend %xsmall;
+ line-height: 1rem;
+ margin-bottom: .5rem;
+ text-align: center;
+ }
+ .use-default-image {
+ @extend %xsmall;
+ text-align: center;
+ &:hover {
+ color: $red;
+ }
+ }
+
+ .project-details-form-data {
+ flex: 1;
+ max-width: 500px;
+ }
+
+ @include breakpoint(tablet) {
+ form {
+ display: block;
+ }
+ }
+}
diff --git a/app/styles/dependencies/mixins/project-card.scss b/app/styles/dependencies/mixins/project-card.scss
new file mode 100644
index 00000000..731c6ca5
--- /dev/null
+++ b/app/styles/dependencies/mixins/project-card.scss
@@ -0,0 +1,75 @@
+@mixin project-card {
+ background: $white;
+ border: 1px solid $whitish;
+ margin: .5rem;
+ .tags-container {
+ display: flex;
+ height: .3rem;
+ }
+ .project-tag {
+ flex: 1;
+ }
+ .project-card-inner {
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+ }
+ .project-card-description {
+ @extend %small;
+ @extend %light;
+ color: $gray;
+ }
+ .project-card-statistics {
+ display: flex;
+ margin-top: auto;
+ svg {
+ @include svg-size(.8rem);
+ fill: $gray-light;
+ }
+ .svg-eye-closed {
+ display: none;
+ }
+ }
+ .statistic {
+ @extend %small;
+ color: $gray-light;
+ display: inline-block;
+ margin-right: .5rem;
+ &.active {
+ color: $primary;
+ svg {
+ fill: $primary;
+ }
+ }
+ }
+ .project-card-header {
+ align-items: flex-start;
+ display: flex;
+ }
+ .project-card-logo {
+ flex-basis: 50px;
+ min-width: 50px;
+ margin-right: .5rem;
+ img {
+ width: 100%;
+ }
+ }
+ .project-card-name {
+ line-height: .9;
+ a {
+ @extend %large;
+ @extend %large;
+ color: $primary;
+ &:hover {
+ color: $primary-light;
+ }
+ }
+ }
+ .look-for-people {
+ svg {
+ @include svg-size(1rem);
+ fill: $gray-light;
+ margin: 0 .5rem;
+ }
+ }
+}
diff --git a/app/styles/layout/auth.scss b/app/styles/layout/auth.scss
index 468ba9f2..1898df12 100644
--- a/app/styles/layout/auth.scss
+++ b/app/styles/layout/auth.scss
@@ -14,13 +14,11 @@
flex-basis: 400px;
}
.logo-svg {
- max-height: 140px;
- padding: 0 33%;
text-align: center;
width: 100%;
svg {
- max-height: 100%;
- max-width: 100%;
+ height: 8rem;
+ width: 8rem;
}
}
.logo {
diff --git a/app/styles/layout/ticket-detail.scss b/app/styles/layout/ticket-detail.scss
index 061a81f5..a2d5bedd 100644
--- a/app/styles/layout/ticket-detail.scss
+++ b/app/styles/layout/ticket-detail.scss
@@ -10,9 +10,11 @@
.us-title {
@extend %large;
@extend %text;
+ align-items: center;
background: $whitish;
+ display: flex;
flex: 1;
- padding: 1rem;
+ padding: .5rem;
position: relative;
transition: all .2s linear;
&.blocked {
@@ -64,11 +66,14 @@
flex-grow: 1;
}
.us-title-text {
+ @extend %larger;
+ @extend %text;
align-content: center;
align-items: center;
display: flex;
+ flex: 1;
margin-bottom: 0;
- max-width: 94%;
+ max-width: 92%;
}
.us-title-text:hover {
.icon-edit {
@@ -77,16 +82,14 @@
}
}
.us-number {
- @extend %xlarge;
- @extend %title;
+ @extend %text;
color: $gray-light;
flex-shrink: 0;
line-height: 2.2rem;
margin-right: .5rem;
}
.us-name {
- @extend %xlarge;
- color: $grayer;
+ color: $gray;
display: inline-block;
line-height: 2.2rem;
padding-right: 1rem;
diff --git a/app/styles/modules/admin/admin-project-profile.scss b/app/styles/modules/admin/admin-project-profile.scss
index 044cdb76..5e4e0483 100644
--- a/app/styles/modules/admin/admin-project-profile.scss
+++ b/app/styles/modules/admin/admin-project-profile.scss
@@ -1,74 +1,106 @@
+@import '../dependencies/mixins/profile-form';
+
.project-details {
- form {
- max-width: 700px;
- width: 100%;
+ @include profile-form;
+ .looking-for-people {
+ @extend %light;
+ border-bottom: 1px solid $whitish;
+ border-top: 1px solid $whitish;
+ padding: 1rem 0;
}
- input,
- textarea {
- @extend %title;
+ .looking-for-people-selector {
+ align-items: center;
+ display: flex;
+ svg {
+ @include svg-size();
+ fill: $gray-light;
+ margin-left: .5rem;
+ }
+ .check {
+ margin-left: auto;
+ }
}
- fieldset {
- margin-bottom: 1rem;
+ .looking-for-people-reason {
+ display: block;
+ margin-top: 1rem;
+ &.ng-hide-remove-active {
+ animation: dropdownFade .3s;
+ }
+ &.ng-hide-add-active {
+ animation: dropdownFade .2s reverse;
+ animation-delay: .1s;
+ }
+ }
+
+ .delete-project {
+ @extend %xsmall;
+ display: block;
+ margin-top: 1rem;
+ text-align: right;
+ &:hover {
+ color: $red;
+ }
+ }
+ .private-or-public {
+ @extend %xsmall;
+ color: $gray-light;
+ margin-bottom: 2rem;
+ svg {
+ @include svg-size(1.1rem);
+ fill: $gray-light;
+ margin-right: .5rem;
+ vertical-align: middle;
+ }
+ }
+
+}
+
+.project-privacy-settings {
+ display: flex;
+ margin-bottom: .5rem;
+ .privacy-option {
+ flex: 1;
+ transition: .2 linear;
+ &:first-child {
+ margin-right: .5rem;
+ }
+ }
+ input[type="radio"] {
+ display: none;
+ }
+ input[type="text"] {
+ display: none;
}
label {
- @extend %title;
- display: block;
- margin-bottom: .2rem;
- }
- textarea {
- height: 10rem;
- }
- .privacy-settings {
- display: flex;
- margin-bottom: 2rem;
- > div {
- flex-basis: 0;
- flex-grow: 1;
- overflow: hidden;
- position: relative;
- &:first-child {
- margin-right: .5rem;
+ background: $whitish;
+ color: $grayer;
+ text-align: center;
+ transition: all .2s linear;
+ &:hover {
+ background: rgba($primary-light, .4);
+ color: $grayer;
+ svg {
+ fill: $grayer;
}
}
- label {
- @extend %title;
- border: 1px solid $gray-light;
- cursor: not-allowed;
- display: block;
- text-align: center;
- transition: all .2s linear;
- span {
- color: $gray-light;
- }
+ svg {
+ @include svg-size(1.1rem);
+ fill: $grayer;
+ margin-left: .5rem;
+ vertical-align: middle;
}
}
- .privacy-project {
- cursor: pointer;
- height: 50px;
- left: -10px;
- opacity: 0;
- position: absolute;
- top: -10px;
- width: 500px;
- z-index: 999;
- }
.privacy-project:checked {
+ label {
background: $primary-light;
- border: 1px solid $primary-light;
- span {
- color: $white;
+ color: $white;
+ svg {
+ @include svg-size(1.1rem);
+ fill: $white;
}
}
- }
- .button-green {
- color: $white;
- display: block;
- text-align: center;
- }
- .delete-project {
- @extend %small;
- display: block;
- margin-top: 1rem;
+ ~input[type="text"] {
+ display: block;
+ }
}
}
diff --git a/app/styles/modules/backlog/taskboard-table.scss b/app/styles/modules/backlog/taskboard-table.scss
index 6b6b5983..dae0c4fb 100644
--- a/app/styles/modules/backlog/taskboard-table.scss
+++ b/app/styles/modules/backlog/taskboard-table.scss
@@ -68,9 +68,11 @@ $column-margin: 0 10px 0 0;
position: absolute;
}
.task-colum-name {
- @extend %large;
+ @extend %medium;
+ align-items: center;
background: $whitish;
border-top: 3px solid $gray-light;
+ color: $gray;
display: flex;
flex-basis: $column-width;
flex-grow: $column-flex;
diff --git a/app/styles/modules/home-project.scss b/app/styles/modules/home-project.scss
index 187d441c..a58aab65 100644
--- a/app/styles/modules/home-project.scss
+++ b/app/styles/modules/home-project.scss
@@ -1,24 +1,35 @@
.single-project {
.single-project-intro {
+ display: flex;
margin-bottom: 2rem;
}
+ .project-logo {
+ margin-right: 1rem;
+ width: 6rem;
+ img {
+ width: 100%;
+ }
+ }
+ .single-project-title-wrapper {
+ flex: 1;
+ }
.intro-options {
align-items: center;
display: flex;
justify-content: space-between;
+ margin-bottom: .5rem;
+ }
+ .intro-title {
+ align-items: center;
+ display: flex;
}
h1 {
color: $primary;
display: inline-block;
line-height: 1.2;
margin-bottom: 0;
- margin-right: 3rem;
vertical-align: middle;
}
- .private {
- font-size: 1rem;
- vertical-align: super;
- }
.like-watch-container {
margin-left: auto;
}
@@ -34,6 +45,7 @@
.description {
@extend %light;
@extend %medium;
+ margin: 0;
}
.project-data {
display: flex;
@@ -60,6 +72,18 @@
max-width: 960px;
width: 0;
}
+ .looking-for-people {
+ img {
+ width: 100%;
+ }
+ h3 {
+ @extend %small;
+ }
+ p {
+ @extend %small;
+ @extend %light;
+ }
+ }
.involved-data {
flex-basis: 220px;
width: 220px;
@@ -70,8 +94,8 @@
flex-wrap: wrap;
margin-bottom: 1rem;
li {
+ flex-basis: 24%;
margin-right: .14rem;
- width: 24%;
&:nth-child(4n) {
margin-right: 0;
}
diff --git a/app/styles/modules/kanban/kanban-table.scss b/app/styles/modules/kanban/kanban-table.scss
index 544e08f1..817c8a58 100644
--- a/app/styles/modules/kanban/kanban-table.scss
+++ b/app/styles/modules/kanban/kanban-table.scss
@@ -65,9 +65,11 @@ $column-margin: 0 10px 0 0;
position: absolute;
}
.task-colum-name {
- @extend %large;
+ @extend %medium;
+ align-items: center;
background: $whitish;
border-top: 3px solid $gray-light;
+ color: $gray;
display: flex;
flex-basis: $column-width;
flex-grow: $column-flex;
diff --git a/app/styles/modules/user-settings/user-profile.scss b/app/styles/modules/user-settings/user-profile.scss
index 1c10d313..16f492d1 100644
--- a/app/styles/modules/user-settings/user-profile.scss
+++ b/app/styles/modules/user-settings/user-profile.scss
@@ -1,81 +1,10 @@
+@import '../dependencies/mixins/profile-form';
+
.user-profile {
- form {
- max-width: 700px;
+ @include profile-form;
+ max-width: 780px;
+ .submit-button {
width: 100%;
- .container {
- display: flex;
- }
- .avatar-container {
- flex-basis: 0;
- flex-grow: 1;
- margin-right: 1rem;
- .image-container {
- position: relative;
- }
- .avatar {
- border-radius: 8%;
- width: 100%;
- }
- .overlay {
- align-items: center;
- background: rgba($blackish, .8);
- bottom: 0;
- display: flex;
- left: 0;
- position: absolute;
- right: 0;
- top: 0;
- width: 100%;
- }
- .loading-spinner {
- @extend %loading-spinner;
- border: 0;
- min-height: 3rem;
- min-width: 3rem;
- transform-origin: center center;
- }
- p {
- @extend %xsmall;
- line-height: .8rem;
- margin-bottom: .3rem;
- text-align: center;
- }
- span {
- @extend %bold;
- }
- .use-gravatar {
- @extend %small;
- cursor: pointer;
- display: inline-block;
- text-align: center;
- width: 100%;
- }
- }
- .data {
- flex-basis: 0;
- flex-grow: 3;
- }
- }
- fieldset {
- margin-bottom: 1rem;
- }
- .submit {
- margin-top: 2rem;
- }
- label {
- @extend %title;
- display: block;
- margin-bottom: .5rem;
- }
- textarea {
- min-height: 7rem;
- }
- .button-green {
- color: $white;
- cursor: pointer;
- display: block;
- padding: 12px;
- text-align: center;
}
.delete-account {
@extend %small;
diff --git a/app/svg/activity.svg b/app/svg/activity.svg
new file mode 100644
index 00000000..66e8684f
--- /dev/null
+++ b/app/svg/activity.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/svg/discover.svg b/app/svg/discover.svg
new file mode 100644
index 00000000..5334c1bb
--- /dev/null
+++ b/app/svg/discover.svg
@@ -0,0 +1,3 @@
+
diff --git a/app/svg/help.svg b/app/svg/help.svg
new file mode 100644
index 00000000..b822b8fd
--- /dev/null
+++ b/app/svg/help.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/app/svg/recruit.svg b/app/svg/recruit.svg
new file mode 100644
index 00000000..fa8dcf32
--- /dev/null
+++ b/app/svg/recruit.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/svg/search.svg b/app/svg/search.svg
new file mode 100644
index 00000000..8c58b6ec
--- /dev/null
+++ b/app/svg/search.svg
@@ -0,0 +1,5 @@
+
diff --git a/app/svg/team.svg b/app/svg/team.svg
new file mode 100644
index 00000000..fc0fc652
--- /dev/null
+++ b/app/svg/team.svg
@@ -0,0 +1,4 @@
+
diff --git a/app/themes/taiga/variables.scss b/app/themes/taiga/variables.scss
index 4e92c60e..4de07219 100755
--- a/app/themes/taiga/variables.scss
+++ b/app/themes/taiga/variables.scss
@@ -34,7 +34,6 @@ $yellow-pear: #bbe831;
$tribe-primary: #98e0eb;
$tribe-secondary: #107a8a;
-
$top-icon-color: #11241f;
$dropdown-color: rgba(darken($grayer, 20%), 1);
diff --git a/conf.e2e.js b/conf.e2e.js
index e0b11b37..bca55590 100644
--- a/conf.e2e.js
+++ b/conf.e2e.js
@@ -40,7 +40,8 @@ exports.config = {
kanban: "e2e/suites/kanban.e2e.js",
projectHome: "e2e/suites/project-home.e2e.js",
search: "e2e/suites/search.e2e.js",
- team: "e2e/suites/team.e2e.js"
+ team: "e2e/suites/team.e2e.js",
+ discover: "e2e/suites/discover/*.e2e.js"
},
onPrepare: function() {
// track mouse movements
diff --git a/e2e/helpers/discover-helper.js b/e2e/helpers/discover-helper.js
new file mode 100644
index 00000000..2e6d8cfe
--- /dev/null
+++ b/e2e/helpers/discover-helper.js
@@ -0,0 +1,87 @@
+var utils = require('../utils');
+
+var helper = module.exports;
+
+helper.liked = function() {
+ return $('tg-most-liked');
+};
+
+helper.active = function() {
+ return $('tg-most-active');
+};
+
+helper.featured = function() {
+ return $('tg-featured-projects');
+};
+
+helper.likedProjects = function() {
+ return helper.liked().$$('.highlighted-project');
+};
+
+helper.activeProjects = function() {
+ return helper.active().$$('.highlighted-project');
+};
+
+helper.featuredProjects = function() {
+ return helper.featured().$$('.featured-project');
+};
+
+helper.rearrangeLike = function(index) {
+ helper.liked().$('.current-filter').click();
+
+ helper.liked().$$('.filter-list li').get(index).click();
+};
+
+helper.getLikeFilterText = function(index) {
+ return helper.liked().$('.current-filter').getText();
+};
+
+helper.rearrangeActive = function(index) {
+ helper.active().$('.current-filter').click();
+
+ helper.active().$$('.filter-list li').get(index).click();
+};
+
+helper.getActiveFilterText = function(index) {
+ return helper.active().$('.current-filter').getText();
+};
+
+helper.searchFilter = function(index) {
+ return $$('.searchbox-filters label').get(index).click();
+};
+
+helper.searchProjectsList = function() {
+ return $('.project-list');
+};
+
+helper.searchProjects = function() {
+ return helper.searchProjectsList().$$('li');
+};
+
+helper.searchInput = function() {
+ return $('.searchbox input');
+};
+
+helper.sendSearch = function() {
+ return $('.search-button').click();
+};
+
+helper.mostLiked = function() {
+ $$('.discover-search-filter').get(0).click();
+};
+
+helper.mostActived = function() {
+ $$('.discover-search-filter').get(1).click();
+};
+
+helper.searchOrder = function(index) {
+ $$('.filter-list a').get(index).click();
+};
+
+helper.orderSelectorWrapper = function() {
+ return $('.discover-search-subfilter');
+};
+
+helper.clearOrder = function() {
+ helper.orderSelectorWrapper().$('.results a').click();
+};
diff --git a/e2e/helpers/project-detail-helper.js b/e2e/helpers/project-detail-helper.js
new file mode 100644
index 00000000..096dab63
--- /dev/null
+++ b/e2e/helpers/project-detail-helper.js
@@ -0,0 +1,27 @@
+var utils = require('../utils');
+
+var helper = module.exports;
+
+helper.lookingForPeople = function() {
+ return $$('.looking-for-people input').get(0);
+};
+
+helper.lookingForPeopleReason = function() {
+ return $$('.looking-for-people-reason input').get(0);
+};
+
+helper.toggleIsLookingForPeople = function() {
+ helper.lookingForPeople().click();
+};
+
+helper.editLogo = function() {
+ let inputFile = $('#logo-field');
+
+ var fileToUpload = utils.common.uploadImagePath();
+
+ return utils.common.uploadFile(inputFile, fileToUpload);
+};
+
+helper.getLogoSrc = function() {
+ return $('.image-container .image');
+};
diff --git a/e2e/suites/admin/project/project-detail.e2e.js b/e2e/suites/admin/project/project-detail.e2e.js
index 827b562c..2ebb94a1 100644
--- a/e2e/suites/admin/project/project-detail.e2e.js
+++ b/e2e/suites/admin/project/project-detail.e2e.js
@@ -6,6 +6,8 @@ var chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);
var expect = chai.expect;
+var adminHelper = require('../../../helpers/project-detail-helper');
+
describe('project detail', function() {
before(async function(){
browser.get(browser.params.glob.host + 'project/project-0/admin/project-profile/details');
@@ -40,5 +42,40 @@ describe('project detail', function() {
$('button[type="submit"]').click();
expect(utils.notifications.success.open()).to.be.eventually.equal(true);
+
+ await utils.notifications.success.close();
+ });
+
+ it('looking for people', async function() {
+ let checked = !! await adminHelper.lookingForPeople().getAttribute('checked');
+
+ if(checked) {
+ adminHelper.toggleIsLookingForPeople();
+ }
+
+ adminHelper.toggleIsLookingForPeople();
+
+ adminHelper.lookingForPeopleReason().sendKeys('looking for people reason');
+
+ $('button[type="submit"]').click();
+
+ checked = !! await adminHelper.lookingForPeople().getAttribute('checked');
+
+ expect(checked).to.be.true;
+ expect(utils.notifications.success.open()).to.be.eventually.equal(true);
+ });
+
+ it('edit logo', async function() {
+ let imageContainer = $('.image-container');
+
+ let htmlChanges = await utils.common.outerHtmlChanges(imageContainer);
+
+ adminHelper.editLogo();
+
+ await htmlChanges();
+
+ let src = await adminHelper.getLogoSrc().getAttribute('src');
+
+ expect(src).to.contains('upload-image-test.png');
});
});
diff --git a/e2e/suites/discover/discover-home.e2e.js b/e2e/suites/discover/discover-home.e2e.js
new file mode 100644
index 00000000..96f1900b
--- /dev/null
+++ b/e2e/suites/discover/discover-home.e2e.js
@@ -0,0 +1,63 @@
+var utils = require('../../utils');
+var discoverHelper = require('../../helpers/discover-helper');
+
+var chai = require('chai');
+var chaiAsPromised = require('chai-as-promised');
+
+chai.use(chaiAsPromised);
+var expect = chai.expect;
+
+
+describe('discover', () => {
+ before(async () => {
+ browser.get(browser.params.glob.host + 'discover');
+ await utils.common.waitLoader();
+ });
+
+ it('screenshot', async () => {
+ await utils.common.takeScreenshot("discover", "discover-home");
+ });
+
+ describe('most liked', () => {
+ it('has projects', () => {
+ let projects = discoverHelper.likedProjects();
+
+ expect(projects.count()).to.be.eventually.above(0);
+ });
+
+ it('rearrange', () => {
+ discoverHelper.rearrangeLike(3);
+
+ let filterText = discoverHelper.getLikeFilterText();
+ let projects = discoverHelper.likedProjects();
+
+ expect(filterText).to.be.eventually.equal('All time');
+ expect(projects.count()).to.be.eventually.equal(5);
+
+ });
+ });
+
+ describe('most active', () => {
+ it('has projects', () => {
+ let projects = discoverHelper.activeProjects();
+
+ expect(projects.count()).to.be.eventually.above(0);
+ });
+
+ it('rearrange', () => {
+ discoverHelper.rearrangeActive(3);
+
+ let filterText = discoverHelper.getActiveFilterText();
+ let projects = discoverHelper.activeProjects();
+
+ expect(filterText).to.be.eventually.equal('All time');
+ expect(projects.count()).to.be.eventually.equal(5);
+ });
+ });
+
+ it('featured projects', () => {
+ let projects = discoverHelper.featuredProjects();
+
+ expect(projects.count()).to.be.eventually.above(0);
+ });
+});
diff --git a/e2e/suites/discover/discover-search.e2e.js b/e2e/suites/discover/discover-search.e2e.js
new file mode 100644
index 00000000..b51f3a45
--- /dev/null
+++ b/e2e/suites/discover/discover-search.e2e.js
@@ -0,0 +1,123 @@
+var utils = require('../../utils');
+var discoverHelper = require('../../helpers/discover-helper');
+
+var chai = require('chai');
+var chaiAsPromised = require('chai-as-promised');
+
+chai.use(chaiAsPromised);
+var expect = chai.expect;
+
+
+describe('discover search', () => {
+ before(async () => {
+ browser.get(browser.params.glob.host + 'discover/search');
+ await utils.common.waitLoader();
+ });
+
+ it('screenshot', async () => {
+ await utils.common.takeScreenshot("discover", "discover-search");
+ });
+
+ describe('top bar', async () => {
+ after(async () => {
+ browser.get(browser.params.glob.host + 'discover/search');
+ await utils.common.waitLoader();
+ });
+
+ it('filters', async () => {
+ let htmlChanges = await utils.common.outerHtmlChanges(discoverHelper.searchProjectsList());
+
+ discoverHelper.searchFilter(1);
+
+ await htmlChanges();
+
+ let url = await browser.getCurrentUrl();
+
+ let projects = discoverHelper.searchProjects();
+
+ expect(projects.count()).to.be.eventually.above(0);
+ expect(url).to.be.equal(browser.params.glob.host + 'discover/search?filter=kanban');
+ });
+
+ it('search by text', () => {
+ discoverHelper.searchInput().sendKeys('Project Example 0');
+
+ discoverHelper.sendSearch();
+
+ let projects = discoverHelper.searchProjects();
+ expect(projects.count()).to.be.eventually.equal(1);
+ });
+ });
+
+ describe('most liked', async () => {
+ after(async () => {
+ browser.get(browser.params.glob.host + 'discover/search');
+ await utils.common.waitLoader();
+ });
+
+ it('default', async () => {
+ discoverHelper.mostLiked();
+
+ utils.common.takeScreenshot("discover", "discover-search-filter");
+
+ let url = await browser.getCurrentUrl();
+
+ expect(url).to.be.equal(browser.params.glob.host + 'discover/search?order_by=-total_fans_last_week');
+ });
+
+ it('filter', async () => {
+ discoverHelper.searchOrder(3);
+
+ let projects = discoverHelper.searchProjects();
+
+ let url = await browser.getCurrentUrl();
+
+ expect(projects.count()).to.be.eventually.above(0);
+ expect(url).to.be.equal(browser.params.glob.host + 'discover/search?order_by=-total_fans');
+ });
+
+ it('clear', () => {
+ discoverHelper.clearOrder();
+
+ let orderSelector = discoverHelper.orderSelectorWrapper();
+
+ expect(orderSelector.isPresent()).to.be.eventually.equal(false);
+ });
+ });
+
+ describe('most active', async () => {
+ after(async () => {
+ browser.get(browser.params.glob.host + 'discover/search');
+ await utils.common.waitLoader();
+ });
+
+ it('default', async () => {
+ discoverHelper.mostActived();
+
+ utils.common.takeScreenshot("discover", "discover-search-filter");
+
+ let url = await browser.getCurrentUrl();
+
+ expect(url).to.be.equal(browser.params.glob.host + 'discover/search?order_by=-total_activity_last_week');
+ });
+
+ it('filter', async () => {
+ discoverHelper.searchOrder(3);
+
+ let projects = discoverHelper.searchProjects();
+
+ let url = await browser.getCurrentUrl();
+
+ expect(projects.count()).to.be.eventually.above(0);
+ expect(url).to.be.equal(browser.params.glob.host + 'discover/search?order_by=-total_activity');
+ });
+
+ it('clear', () => {
+ discoverHelper.clearOrder();
+
+ let orderSelector = discoverHelper.orderSelectorWrapper();
+
+ expect(orderSelector.isPresent()).to.be.eventually.equal(false);
+ });
+ });
+});
diff --git a/e2e/utils/common.js b/e2e/utils/common.js
index a0f9511d..c1c4e468 100644
--- a/e2e/utils/common.js
+++ b/e2e/utils/common.js
@@ -163,7 +163,7 @@ common.prepare = function() {
browser.get(browser.params.glob.host);
return common.closeCookies();
-}
+};
common.dragEnd = function(elm) {
return browser.wait(async function() {
diff --git a/gulpfile.js b/gulpfile.js
index 61db5d31..bb3d9a71 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -173,7 +173,8 @@ paths.libs = [
paths.app + "js/jquery-ui.drag-multiple-custom.js",
paths.app + "js/jquery.ui.touch-punch.min.js",
paths.app + "js/tg-repeat.js",
- paths.app + "js/sha1-custom.js"
+ paths.app + "js/sha1-custom.js",
+ paths.app + "js/murmurhash3_gc.js"
];
var isDeploy = argv["_"].indexOf("deploy") !== -1;
diff --git a/run-e2e.js b/run-e2e.js
index 0c2ae066..33af48d8 100644
--- a/run-e2e.js
+++ b/run-e2e.js
@@ -17,7 +17,8 @@ var suites = [
'kanban',
'projectHome',
'search',
- 'team'
+ 'team',
+ 'discover'
];
var lunchSuites = [];