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 = [];