21
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.
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -112,7 +112,7 @@ LoadingDirective = ($loading) ->
|
|||
if showLoading
|
||||
currentLoading = $loading()
|
||||
.target($el)
|
||||
.timeout(50)
|
||||
.timeout(100)
|
||||
.template(template)
|
||||
.scope($scope)
|
||||
.start()
|
||||
|
|
|
@ -175,6 +175,9 @@ urls = {
|
|||
# Application tokens
|
||||
"applications": "/applications"
|
||||
"application-tokens": "/application-tokens"
|
||||
|
||||
# Stats
|
||||
"stats-discover": "/stats/discover"
|
||||
}
|
||||
|
||||
# Initialize api urls service
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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", ->
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.6 KiB |
|
@ -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 <a href="mailto:gary.court@gmail.com">Gary Court</a>
|
||||
* @see http://github.com/garycourt/murmurhash-js
|
||||
* @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
|
@ -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.<br/> 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<br/> points",
|
||||
"DEFINED": "defined<br/> 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.<br />",
|
||||
"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": "<strong>Check your inbox!</strong><br />We have sent a mail to your account<br />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.<br> 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
###
|
||||
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,96 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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'})
|
|
@ -0,0 +1,37 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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 }}
|
|
@ -0,0 +1,36 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,72 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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'}));
|
|
@ -0,0 +1,38 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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 }}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,117 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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
|
|
@ -0,0 +1,37 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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 }}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,33 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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 }}
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# File: highlighted.directive.coffee
|
||||
###
|
||||
|
||||
HighlightedDirective = () ->
|
||||
return {
|
||||
templateUrl: "discover/components/highlighted/highlighted.html",
|
||||
scope: {
|
||||
loading: "=",
|
||||
highlighted: "=",
|
||||
orderBy: "="
|
||||
}
|
||||
}
|
||||
|
||||
HighlightedDirective.$inject = []
|
||||
|
||||
angular.module("taigaDiscover").directive("tgHighlighted", HighlightedDirective)
|
|
@ -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 }}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,79 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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')
|
|
@ -0,0 +1,34 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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 }}
|
|
@ -0,0 +1,49 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,79 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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')
|
|
@ -0,0 +1,34 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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 }}
|
|
@ -0,0 +1,33 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,71 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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');
|
|
@ -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
|
|
@ -0,0 +1,114 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,199 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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()
|
|
@ -0,0 +1,32 @@
|
|||
###
|
||||
# Copyright (C) 2014-2016 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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 }}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# File: discover.module.coffee
|
||||
###
|
||||
|
||||
module = angular.module("taigaDiscover", [])
|
|
@ -0,0 +1,93 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -0,0 +1,178 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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()
|
|
@ -0,0 +1,78 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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
|
|
@ -0,0 +1,32 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -11,7 +11,7 @@
|
|||
display: block;
|
||||
}
|
||||
.title-bar {
|
||||
@extend %title;
|
||||
@extend %light;
|
||||
@extend %larger;
|
||||
align-content: center;
|
||||
background: $whitish;
|
||||
|
|
|
@ -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="#",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
.tab {
|
||||
color: $gray-light;
|
||||
display: inline-block;
|
||||
padding: 1rem 1.25rem;
|
||||
padding: 1rem;
|
||||
&:hover,
|
||||
&.active {
|
||||
color: $grayer;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}}"
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@ services = [
|
|||
"tgTasksResource",
|
||||
"tgIssuesResource",
|
||||
"tgExternalAppsResource",
|
||||
"tgAttachmentsResource"
|
||||
"tgAttachmentsResource",
|
||||
"tgStatsResource"
|
||||
]
|
||||
|
||||
Resources = ($injector) ->
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
###
|
||||
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# 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)
|
|
@ -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
|
||||
|
|
|
@ -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 }}"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
@extend %small;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
color: $white;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
margin-left: .5rem;
|
||||
width: .5rem;
|
||||
svg {
|
||||
height: .5rem;
|
||||
width: .5rem;
|
||||
@include svg-size();
|
||||
}
|
||||
path {
|
||||
fill: $gray-light;
|
||||
|
|
|
@ -40,7 +40,10 @@
|
|||
input {
|
||||
margin-right: .25rem;
|
||||
padding: .4rem;
|
||||
width: 10rem;
|
||||
width: 14rem;
|
||||
+.icon-floppy {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
.tag {
|
||||
@extend %small;
|
||||
|
|
|
@ -84,3 +84,11 @@ svg {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
img {
|
||||
@extend %loading-spinner;
|
||||
max-height: 2rem;
|
||||
max-width: 2rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|