diff --git a/.gitignore b/.gitignore index 18a1d2d1..9fe650a7 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,5 @@ app/coffee/modules/locales/locale*.coffee tags tmp/ app/config/main.coffee -app/plugins/taiga-front-extras scss-lint.log +e2e/screenshots/ diff --git a/.travis.yml b/.travis.yml index 18a06b23..42615756 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ +sudo: false + before_install: - export CHROME_BIN=chromium-browser - export DISPLAY=:99.0 @@ -11,4 +13,4 @@ before_script: - gulp deploy language: node_js node_js: - - "0.12" \ No newline at end of file + - "4.1" diff --git a/.tx/config b/.tx/config index 191c665f..38d247e7 100644 --- a/.tx/config +++ b/.tx/config @@ -1,6 +1,6 @@ [main] host = https://www.transifex.com -lang_map = sr@latin:sr-latn, zh-Hans:zh-hans, zh-Hant:zh-hant +lang_map = sr@latin:sr-latn, zh-Hans:zh-hans, zh-Hant:zh-hant, pt_BR:pt-br [taiga-front.locale-enjson] file_filter = app/locales/locale-.json diff --git a/AUTHORS.rst b/AUTHORS.rst index 8f30a52f..89ed65b2 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -27,3 +27,5 @@ answer newbie questions, and generally made Taiga that much better: - Florian Bezagu - Ryan Swanstrom - Chris Wilson +- Brett Profitt +- Vlad Topala diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef083cc..64311291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,30 @@ # Changelog # +## 1.9.0 Abies Siberica (2015-11-XX) + +### Features +- Ability to create single-line or multi-line custom fields. (thanks to [@artlepool](https://github.com/artlepool)). +- Ability to date custom fields. (thanks to [@artlepool](https://github.com/artlepool)). +- Add custom videoconference system. +- Make burndown chart collapsible at the backlog panel. +- Ability to choose a theme (thanks to [@astagi](https://github.com/astagi)). +- Inline viewing of image attachments (thanks to [@brettp](https://github.com/brettp)). +- Autocomplete for usernames, user stories, tasks, issues, and wiki pages in text areas (thanks to [@brettp](https://github.com/brettp)). +- Support authentication via Application Tokens. +- User onboarding: improve placeholders and add joyriders. +- i18n. + - Add italian (it) translation. + - Add polish (pl) translation. + - Add portuguese (Brazil) (pt_BR) translation. + - Add russian (ru) translation. + +### Misc +- Improve performance: Show cropped images in timelines. +- Caps lock warning in login and register form. +- Lots of small and not so small bugfixes. + + ## 1.8.0 Saracenia Purpurea (2015-06-18) ### Features diff --git a/README.md b/README.md index dafbec35..edf97336 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,107 @@ ![Kaleidos Project](http://kaleidos.net/static/img/badge.png "Kaleidos Project") [![Managed with Taiga](https://taiga.io/media/support/attachments/article-22/banner-gh.png)](https://taiga.io "Managed with Taiga") [![Build Status](https://travis-ci.org/taigaio/taiga-front.svg?branch=public-header-bar)](https://travis-ci.org/taigaio/taiga-front) +[![Dependency Status](https://www.versioneye.com/user/projects/561ba659a193340f280013f4/badge.svg?style=flat)](https://www.versioneye.com/user/projects/561ba659a193340f280013f4) + ## Get the compiled version ## You can get the compiled version of this code in the [taiga-front-dist](http://github.com/taigaio/taiga-front-dist) repository -## Setup initial environment ## + +## Contribute to Taiga ## + +#### Where to start #### + +There are many different ways to contribute to Taiga's development, just find the one that best fits with your skills. Examples of contributions we would love to receive include: + +- **Code patches** +- **Documentation improvements** +- **Translations** +- **Bug reports** +- **Patch reviews** +- **UI enhancements** + +Big features are also welcome but if you want to see your contributions included in Taiga codebase we strongly recommend you start by initiating a chat though our [mailing list](http://groups.google.co.uk/d/forum/taigaio) + + +#### License #### + +Every code patch accepted in taiga codebase is licensed under [AGPL v3.0](http://www.gnu.org/licenses/agpl-3.0.html). You should must be careful to not include any code that can not be licensed under this license. + +Please read carefully [our license](https://github.com/taigaio/taiga-front/blob/master/LICENSE) and ask us if you have any questions. + + +#### Bug reports, enhancements and support #### + +If you **nedd help to setup Taiga**, you want to **talk about some cool enhancemnt** or you have **some questions** please write us to our [mailing list](http://groups.google.com/d/forum/taigaio). + +If you **find a bug** in Taiga you can always report it: + +- in our [mailing list](http://groups.google.com/d/forum/taigaio). +- in [github issues](https://github.com/taigaio/taiga-front/issues). +- send us a mail to support@taiga.io if is a bug related to tree.taiga.io. +- send a mail to security@taiga.io if is a **security bug**. + +One of our fellow Taiga developers will search, find and hunt it as soon as possible. + +Please, before reporting an bug write down how can we reproduce it, your operating system, your browser and version, and if it's possible, a screenshot. Sometimes it take less time to fix a bug if the developer know how to find it and we will solve your problem as fast as possible. + + +#### Documentation improvements #### + +We are gathering lots of information from our users to build and enhance our documentation. If you are the documentation to install or develop with Taiga and find any mistakes, omissions or confused sequences, it is enormously helpful to report it. Or better still, if you believe you can author additions, please make a pull-request to taiga project. + +Currently, we have authored three main documentation hubs: + +- **[API Docs](https://github.com/taigaio/taiga-doc)**: Our API documentation and reference for developing from Taiga API. +- **[Installation Guide](https://github.com/taigaio/taiga-doc)**: If you need to install Taiga on your own server, this is the place to find some guides. +- **[Taiga Support](https://github.com/taigaio/taiga-doc)**: This page is intended to be the support reference page for the users. If you find any mistake, please report it. + + +#### Translation #### + +We are ready now to accept your help translating Taiga. It's easy (and fun!) just access to our team of translators with the link below, set up an account in Transifex and start contributing. Join us to make sure your language is covered! **[Help Taiga to trasnlate content](https://www.transifex.com/signup/ "Help Taiga to trasnlatecontent")** + + +#### Code patches #### + +Taiga will always be glad to receive code patches to update, fix or improve its code. + +If you know how to improve our code base or you found a bug, a security vulnerabilities a performance issue and you think you can solve, we will be very happy to accept your pull-request. If your code requires considerable changes, we recommend you first talk to us directly. We will find the best way to help. + + +#### UI enhancements #### + +Taiga is made for developers and designers. We care enormously about UI because usability and design are both critical aspects of the Taiga experience. + +There are two possible ways to contribute to our UI: +- **Bugs**: If you find a bug regarding front-end, please report it as previously indicated in the Bug reports section or send a pull-request as indicated in the Code Patches section. +- **Enhancements**: If its a design or UX bug or enhancement we will love to receive your feedback. Please send us your enhancement, with the reason and, if it's possible, an example. Our design and UX team will review your enhancement and fix it as soon as possible. We recommend you to use our [mailing list](http://groups.google.co.uk/d/forum/taigaio){target="_blank"} so we can have a lot of different opinions and debate. +- **Language Localization**: We are eager to offer localized versions of Taiga. Some members of the community have already volunteered to work to provide a variety of languages. We are working to implement some changes to allow for this and expect to accept these requests in the near future. + + + +## Community ## + +[Taiga has a mailing list](http://groups.google.com/d/forum/taigaio). Feel free to join it and ask any questions you may have. + +To subscribe for announcements of releases, important changes and so on, please follow [@taigaio](https://twitter.com/taigaio) on Twitter. + + +## Donations ## + +We are grateful for your emails volunteering donations to Taiga. We feel comfortable accepting them under these conditions: The first that we will only do so while we are in the current beta / pre-revenue stage and that whatever money is donated will go towards a bounty fund. Starting Q2 2015 we will be engaging much more actively with our community to help further the development of Taiga, and we will use these donations to reward people working alongside us. + +If you wish to make a donation to this Taiga fund, you can do so via http://www.paypal.com using the email: eposner@taiga.io + + +## Setup ## + +All the information about the different installation methods (production, development, vagrant, docker...) can be found here http://taigaio.github.io/taiga-doc/dist/#_installation_guide. + +#### Initial dev env #### Install requirements: @@ -19,36 +113,62 @@ You can install Ruby through the apt package manager, rbenv, or rvm. Install Sass through your **Terminal or Command Prompt**. ``` -$ gem install sass scss-lint -$ export PATH="~/.gem/ruby/2.1.0/bin:$PATH" -$ sass -v // should return Sass 3.3.8 (Maptastic Maple) +gem install sass scss-lint +export PATH="~/.gem/ruby/2.1.0/bin:$PATH" +sass -v # should return Sass 3.3.8 (Maptastic Maple) ``` Complete process for all OS at: http://sass-lang.com/install **Node + Bower + Gulp** +We recommend using [nvm](https://github.com/creationix/nvmv) to manage diferent node versions ``` -$ sudo npm install -g gulp -$ sudo npm install -g bower -$ npm install -$ bower install -$ gulp +npm install -g gulp +npm install -g bower +npm install +bower install +gulp ``` And go in your browser to: http://localhost:9001/ -All the information about the different installation methods (production, development, vagrant, docker...) can be found here http://taigaio.github.io/taiga-doc/dist/#_installation_guide. +#### E2E test #### -## Community ## +If you want to run e2e tests -[Taiga has a mailing list](http://groups.google.com/d/forum/taigaio). Feel free to join it and ask any questions you may have. +``` +npm install -g protractor +npm install -g mocha +npm install -g babel -To subscribe for announcements of releases, important changes and so on, please follow [@taigaio](https://twitter.com/taigaio) on Twitter. +webdriver-manager update +``` -## Donations ## +## Tests ## -We are grateful for your emails volunteering donations to Taiga. We feel comfortable accepting them under these conditions: The first that we will only do so while we are in the current beta / pre-revenue stage and that whatever money is donated will go towards a bounty fund. Starting Q2 2015 we will be engaging much more actively with our community to help further the development of Taiga, and we will use these donations to reward people working alongside us. +#### Unit tests #### -If you wish to make a donation to this Taiga fund, you can do so via http://www.paypal.com using the email: eposner@taiga.io +- To run **unit tests** + ``` + gulp + ``` + ``` + npm test + ``` + +#### E2E tests #### + +- To run **e2e tests** you need [taiga-back](https://github.com/taigaio/taiga-back) running and + + ``` + gulp + ``` + ``` + webdriver-manager start + ``` + ``` + protractor conf.e2e.js --suite=auth # To tests authentication + protractor conf.e2e.js --suite=full # To test all the platform authenticated + ``` diff --git a/app-loader/app-loader.coffee b/app-loader/app-loader.coffee index fba45384..47f6b4c4 100644 --- a/app-loader/app-loader.coffee +++ b/app-loader/app-loader.coffee @@ -4,6 +4,8 @@ window.taigaConfig = { "eventsUrl": null, "debug": true, "defaultLanguage": "en", + "themes": ["taiga", "material-design", "high-contrast"], + "defaultTheme": "taiga", "publicRegisterEnabled": true, "feedbackEnabled": true, "privacyPolicyUrl": null, diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index 3c52563e..51d14005 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -66,9 +66,11 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven access: { requiresLogin: true }, + loader: true, title: "HOME.PAGE_TITLE", + loader: true, description: "HOME.PAGE_DESCRIPTION", - loader: true + joyride: "dashboard" } ) @@ -100,7 +102,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven { templateUrl: "search/search.html", reloadOnSearch: false, - section: "search" + section: "search", + loader: true } ) @@ -108,7 +111,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven { templateUrl: "backlog/backlog.html", loader: true, - section: "backlog" + section: "backlog", + joyride: "backlog" } ) @@ -116,7 +120,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven { templateUrl: "kanban/kanban.html", loader: true, - section: "kanban" + section: "kanban", + joyride: "kanban" } ) @@ -333,29 +338,25 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $routeProvider.when("/login", { templateUrl: "auth/login.html", - title: "LOGIN.PAGE_TITLE" - description: "LOGIN.PAGE_DESCRIPTION" + title: "LOGIN.PAGE_TITLE", + description: "LOGIN.PAGE_DESCRIPTION", + disableHeader: true } ) $routeProvider.when("/register", { templateUrl: "auth/register.html", title: "REGISTER.PAGE_TITLE", - description: "REGISTER.PAGE_DESCRIPTION" + description: "REGISTER.PAGE_DESCRIPTION", + disableHeader: true } ) $routeProvider.when("/forgot-password", { templateUrl: "auth/forgot-password.html", title: "FORGOT_PASSWORD.PAGE_TITLE", - description: "FORGOT_PASSWORD.PAGE_DESCRIPTION" - } - ) - $routeProvider.when("/change-password", - { - templateUrl: "auth/change-password-from-recovery.html", - title: "CHANGE_PASSWORD.PAGE_TITLE", - description: "CHANGE_PASSWORD.PAGE_TITLE", + description: "FORGOT_PASSWORD.PAGE_DESCRIPTION", + disableHeader: true } ) $routeProvider.when("/change-password/:token", @@ -363,13 +364,26 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven templateUrl: "auth/change-password-from-recovery.html", title: "CHANGE_PASSWORD.PAGE_TITLE", description: "CHANGE_PASSWORD.PAGE_TITLE", + disableHeader: true } ) $routeProvider.when("/invitation/:token", { templateUrl: "auth/invitation.html", title: "INVITATION.PAGE_TITLE", - description: "INVITATION.PAGE_DESCRIPTION" + description: "INVITATION.PAGE_DESCRIPTION", + disableHeader: true + } + ) + $routeProvider.when("/external-apps", + { + templateUrl: "external-apps/external-app.html", + title: "EXTERNAL_APP.PAGE_TITLE", + description: "EXTERNAL_APP.PAGE_DESCRIPTION", + controller: "ExternalApp", + controllerAs: "vm", + disableHeader: true, + mobileViewport: true } ) @@ -405,13 +419,13 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven # Add next param when user try to access to a secction need auth permissions. authHttpIntercept = ($q, $location, $navUrls, $lightboxService) -> httpResponseError = (response) -> - if response.status == 0 + if response.status == 0 || response.status == -1 $lightboxService.closeAll() $location.path($navUrls.resolve("error")) $location.replace() - else if response.status == 401 - nextPath = $location.path() - $location.url($navUrls.resolve("login")).search("next=#{nextPath}") + else if response.status == 401 and $location.url().indexOf('/login') == -1 + nextUrl = encodeURIComponent($location.url()) + $location.url($navUrls.resolve("login")).search("next=#{nextUrl}") return $q.reject(response) @@ -499,9 +513,13 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven .addInterpolation('$translateMessageFormatInterpolation') .preferredLanguage(preferedLangCode) - if not window.taigaConfig.debugInfo - $translateProvider.fallbackLanguage(preferedLangCode) + $translateProvider.fallbackLanguage(preferedLangCode) + # decoratos + decorators = _.where(@.taigaContribPlugins, {"type": "decorator"}) + + _.each decorators, (decorator) -> + $provide.decorator decorator.provider, decorator.decorator # decoratos decorators = _.where(@.taigaContribPlugins, {"type": "decorator"}) @@ -544,7 +562,7 @@ i18nInit = (lang, $translate) -> checksley.updateMessages('default', messages) -init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, projectService, loaderService) -> +init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, projectService, loaderService, navigationBarService) -> $log.debug("Initialize application") # Taiga Plugins @@ -588,7 +606,7 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na projectService.setSection(next.section) if next.params.pslug - projectService.setProject(next.params.pslug) + projectService.setProjectBySlug(next.params.pslug) else projectService.cleanProject() @@ -597,6 +615,17 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na description = $translate.instant(next.description or "") appMetaService.setAll(title, description) + if next.mobileViewport + appMetaService.addMobileViewport() + else + appMetaService.removeMobileViewport() + + if next.disableHeader + navigationBarService.disableHeader() + else + navigationBarService.enableHeader() + +pluginsWithModule = _.filter(@.taigaContribPlugins, (plugin) -> plugin.module) pluginsWithModule = _.filter(@.taigaContribPlugins, (plugin) -> plugin.module) @@ -634,6 +663,7 @@ modules = [ "taigaProfile", "taigaHome", "taigaUserTimeline", + "taigaExternalApps", # template cache "templates", @@ -641,6 +671,7 @@ modules = [ # Vendor modules "ngRoute", "ngAnimate", + "ngAria", "pascalprecht.translate", "infinite-scroll", "tgRepeat" @@ -673,5 +704,7 @@ module.run([ "tgAppMetaService", "tgProjectService", "tgLoader", + "tgNavigationBarService", + "$route", init ]) diff --git a/app/coffee/classes.coffee b/app/coffee/classes.coffee index 7e995c09..e1d8b9de 100644 --- a/app/coffee/classes.coffee +++ b/app/coffee/classes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/admin.coffee b/app/coffee/modules/admin.coffee index 6087e2e9..e556fcca 100644 --- a/app/coffee/modules/admin.coffee +++ b/app/coffee/modules/admin.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/admin/lightboxes.coffee b/app/coffee/modules/admin/lightboxes.coffee index c5fb9133..68fd0705 100644 --- a/app/coffee/modules/admin/lightboxes.coffee +++ b/app/coffee/modules/admin/lightboxes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -41,7 +41,7 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, $loading, lightboxService, template = _.template("""
- data-required="true" <% } %> data-type="email" />
diff --git a/app/coffee/modules/admin/memberships.coffee b/app/coffee/modules/admin/memberships.coffee index 505419fe..98279a35 100644 --- a/app/coffee/modules/admin/memberships.coffee +++ b/app/coffee/modules/admin/memberships.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -210,15 +210,17 @@ module.directive("tgMemberships", ["$tgTemplate", "$compile", MembershipsDirecti ## Member Avatar Directive ############################################################################# -MembershipsRowAvatarDirective = ($log, $template) -> +MembershipsRowAvatarDirective = ($log, $template, $translate) -> template = $template.get("admin/memberships-row-avatar.html", true) link = ($scope, $el, $attrs) -> + pending = $translate.instant("ADMIN.MEMBERSHIP.STATUS_PENDING") render = (member) -> ctx = { full_name: if member.full_name then member.full_name else "" email: if member.user_email then member.user_email else member.email imgurl: if member.photo then member.photo else "/images/unnamed.png" + pending: if !member.is_user_active then pending else "" } html = template(ctx) @@ -236,7 +238,7 @@ MembershipsRowAvatarDirective = ($log, $template) -> return {link: link} -module.directive("tgMembershipsRowAvatar", ["$log", "$tgTemplate", MembershipsRowAvatarDirective]) +module.directive("tgMembershipsRowAvatar", ["$log", "$tgTemplate", '$translate', MembershipsRowAvatarDirective]) ############################################################################# @@ -357,9 +359,8 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla """ pendingTemplate = """ - - {{'ADMIN.MEMBERSHIP.STATUS_PENDING' | translate}} - + + {{'ADMIN.MEMBERSHIP.RESEND' | translate}} @@ -402,9 +403,9 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla defaultMsg = $translate.instant("ADMIN.MEMBERSHIP.DEFAULT_DELETE_MESSAGE", {email: member.email}) message = if member.user then member.full_name else defaultMsg - $confirm.askOnDelete(title, message).then (finish) -> - onSuccess = -> - finish() + $confirm.askOnDelete(title, message).then (askResponse) -> + onSuccess = => + askResponse.finish() if $scope.page > 1 && ($scope.count - 1) <= $scope.paginatedBy $ctrl.selectFilter("page", $scope.page - 1) @@ -414,8 +415,8 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla text = $translate.instant("ADMIN.MEMBERSHIP.SUCCESS_DELETE") $confirm.notify("success", null, text) - onError = -> - finish(false) + onError = => + askResponse.finish(false) text = $translate.instant("ADMIN.MEMBERSHIP.ERROR_DELETE", {message: message}) $confirm.notify("error", null, text) diff --git a/app/coffee/modules/admin/nav.coffee b/app/coffee/modules/admin/nav.coffee index 8aeb9631..52dd7920 100644 --- a/app/coffee/modules/admin/nav.coffee +++ b/app/coffee/modules/admin/nav.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee index 605af894..c6721f2b 100644 --- a/app/coffee/modules/admin/project-profile.coffee +++ b/app/coffee/modules/admin/project-profile.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -104,7 +104,7 @@ module.controller("ProjectProfileController", ProjectProfileController) ## Project Profile Directive ############################################################################# -ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, projectService) -> +ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, projectService, currentUserService) -> link = ($scope, $el, $attrs) -> $ctrl = $el.controller() @@ -130,6 +130,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, proje $ctrl.loadInitialData() projectService.fetchProject() + currentUserService.loadProjects() promise.then null, (data) -> currentLoading.finish() @@ -144,7 +145,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, proje return {link:link} module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", "$tgNavUrls", "$tgLocation", - "tgProjectService", ProjectProfileDirective]) + "tgProjectService", "tgCurrentUserService", ProjectProfileDirective]) ############################################################################# @@ -192,9 +193,10 @@ module.directive("tgProjectDefaultValues", ["$tgRepo", "$tgConfirm", "$tgLoading ProjectModulesDirective = ($repo, $confirm, $loading, projectService) -> link = ($scope, $el, $attrs) -> - form = $el.find("form").checksley() submit = => + form = $el.find("form").checksley() return if not form.validate() + target = angular.element(".admin-functionalities .submit-button") currentLoading = $loading() .target(target) @@ -226,7 +228,7 @@ ProjectModulesDirective = ($repo, $confirm, $loading, projectService) -> else $el.find(".videoconference-attributes").addClass("hidden") $scope.project.videoconferences = null - $scope.project.videoconferences_salt = "" + $scope.project.videoconferences_extra_data = "" $scope.$watch "project", (project) -> if project.videoconferences? @@ -357,7 +359,7 @@ class CsvExporterController extends taiga.Controller setCsvUuid: => @scope.csvUuid = @scope.project["#{@.type}_csv_uuid"] - _generateUuid: (finish) => + _generateUuid: (response=null) => promise = @rs.projects["regenerate_#{@.type}_csv_uuid"](@scope.projectId) promise.then (data) => @@ -367,7 +369,7 @@ class CsvExporterController extends taiga.Controller @confirm.notify("error") promise.finally -> - finish() + response.finish() if response return promise regenerateUuid: -> @@ -377,7 +379,7 @@ class CsvExporterController extends taiga.Controller @confirm.ask(title, subtitle).then @._generateUuid else - @._generateUuid(_.identity) + @._generateUuid() class CsvExporterUserstoriesController extends CsvExporterController diff --git a/app/coffee/modules/admin/project-values.coffee b/app/coffee/modules/admin/project-values.coffee index cd0e6d0d..6b95c319 100644 --- a/app/coffee/modules/admin/project-values.coffee +++ b/app/coffee/modules/admin/project-values.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -374,6 +374,27 @@ module.directive("tgColorSelection", ColorSelectionDirective) ## Custom Attributes Controller ############################################################################# +# Custom attributes types (see taiga-back/taiga/projects/custom_attributes/choices.py) +TEXT_TYPE = "text" +MULTILINE_TYPE = "multiline" +DATE_TYPE = "date" + + +TYPE_CHOICES = [ + { + key: TEXT_TYPE, + name: "ADMIN.CUSTOM_FIELDS.FIELD_TYPE_TEXT" + }, + { + key: MULTILINE_TYPE, + name: "ADMIN.CUSTOM_FIELDS.FIELD_TYPE_MULTI" + }, + { + key: DATE_TYPE, + name: "ADMIN.CUSTOM_FIELDS.FIELD_TYPE_DATE" + } +] + class ProjectCustomAttributesController extends mixOf(taiga.Controller, taiga.PageMixin) @.$inject = [ "$scope", @@ -390,6 +411,8 @@ class ProjectCustomAttributesController extends mixOf(taiga.Controller, taiga.Pa constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appMetaService, @translate) -> + @scope.TYPE_CHOICES = TYPE_CHOICES + @scope.project = {} @rootscope.$on "project:loaded", => @@ -630,13 +653,11 @@ ProjectCustomAttributesDirective = ($log, $confirm, animationFrame, $translate) title = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.DELETE") text = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.CONFIRM_DELETE") - $confirm.ask(title, text, message).then (finish) -> + $confirm.ask(title, text, message).then (response) -> onSucces = -> - $ctrl.loadCustomAttributes().finally -> - finish() + $ctrl.loadCustomAttributes().finally -> response.finish() onError = -> - finish(false) $confirm.notify("error", null, "We have not been able to delete '#{message}'.") $ctrl.deleteCustomAttribute(attr).then(onSucces, onError) diff --git a/app/coffee/modules/admin/roles.coffee b/app/coffee/modules/admin/roles.coffee index b965e559..78b325c5 100644 --- a/app/coffee/modules/admin/roles.coffee +++ b/app/coffee/modules/admin/roles.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -129,7 +129,7 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil return @repo.remove(@scope.role, {moveTo: response.selected}).then onSuccess, onError - setComputable: debounce 2000, -> + _enableComputable: => onSuccess = => @confirm.notify("success") @.loadProject() @@ -140,9 +140,37 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil @repo.save(@scope.role).then onSuccess, onError + _disableComputable: => + askOnSuccess = (response) => + onSuccess = => + response.finish() + @confirm.notify("success") + @.loadProject() + onError = => + response.finish() + @confirm.notify("error") + @scope.role.revert() + @repo.save(@scope.role).then onSuccess, onError + + askOnError = (response) => + @scope.role.revert() + + title = @translate.instant("ADMIN.ROLES.DISABLE_COMPUTABLE_ALERT_TITLE") + subtitle = @translate.instant("ADMIN.ROLES.DISABLE_COMPUTABLE_ALERT_SUBTITLE", { + roleName: @scope.role.name + }) + message = @translate.instant("ADMIN.ROLES.DISABLE_COMPUTABLE_ALERT_MESSAGE") + return @confirm.ask(title, subtitle, message).then askOnSuccess, askOnError + + toggleComputable: debounce 2000, -> + if not @scope.role.computable + @._disableComputable() + else + @._enableComputable() module.controller("RolesController", RolesController) + EditRoleDirective = ($repo, $confirm) -> link = ($scope, $el, $attrs) -> toggleView = -> diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee index 7d36fc4c..8e7e1daf 100644 --- a/app/coffee/modules/admin/third-parties.coffee +++ b/app/coffee/modules/admin/third-parties.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -177,13 +177,13 @@ WebhookDirective = ($rs, $repo, $confirm, $loading, $translate) -> title = $translate.instant("ADMIN.WEBHOOKS.DELETE") message = $translate.instant("ADMIN.WEBHOOKS.WEBHOOK_NAME", {name: webhook.name}) - $confirm.askOnDelete(title, message).then (finish) => + $confirm.askOnDelete(title, message).then (askResponse) => onSucces = -> - finish() + askResponse.finish() $scope.$emit("webhooks:reload") onError = -> - finish(false) + askResponse.finish(false) $confirm.notify("error") $repo.remove(webhook).then(onSucces, onError) diff --git a/app/coffee/modules/auth.coffee b/app/coffee/modules/auth.coffee index 17c13a4a..5bf83c00 100644 --- a/app/coffee/modules/auth.coffee +++ b/app/coffee/modules/auth.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -37,10 +37,15 @@ class AuthService extends taiga.Service "$tgUrls", "$tgConfig", "$translate", - "tgCurrentUserService"] + "tgCurrentUserService", + "tgThemeService"] - constructor: (@rootscope, @storage, @model, @rs, @http, @urls, @config, @translate, @currentUserService) -> + constructor: (@rootscope, @storage, @model, @rs, @http, @urls, @config, @translate, @currentUserService, + @themeService) -> super() + + @._currentTheme = @config.get("defaultTheme") || "taiga" # load on index.jade + userModel = @.getUser() @.setUserdata(userModel) @@ -51,9 +56,18 @@ class AuthService extends taiga.Service else @.userData = null + _getUserTheme: -> + return @rootscope.user?.theme || @config.get("defaultTheme") || "taiga" + + _setTheme: -> + newTheme = @._getUserTheme() + + if @._currentTheme != newTheme + @._currentTheme = newTheme + @themeService.use(@._currentTheme) _setLocales: -> - lang = @rootscope.user.lang || @config.get("defaultLanguage") || "en" + lang = @rootscope.user?.lang || @config.get("defaultLanguage") || "en" @translate.preferredLanguage(lang) # Needed for calls to the api in the correct language @translate.use(lang) # Needed for change the interface in runtime @@ -66,6 +80,9 @@ class AuthService extends taiga.Service user = @model.make_model("users", userData) @rootscope.user = user @._setLocales() + + @._setTheme() + return user return null @@ -78,6 +95,7 @@ class AuthService extends taiga.Service @.setUserdata(user) @._setLocales() + @._setTheme() clear: -> @rootscope.auth = null @@ -117,9 +135,12 @@ class AuthService extends taiga.Service logout: -> @.removeToken() @.clear() - @currentUserService.removeUser() + @._setTheme() + @._setLocales() + + register: (data, type, existing) -> url = @urls.resolve("auth-register") @@ -200,12 +221,12 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $ link = ($scope, $el, $attrs) -> onSuccess = (response) -> if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login") - nextUrl = $routeParams['next'] + nextUrl = decodeURIComponent($routeParams['next']) else nextUrl = $navUrls.resolve("home") $events.setupConnection() - $location.path(nextUrl) + $location.url(nextUrl) onError = (response) -> $confirm.notify("light-error", $translate.instant("LOGIN_FORM.ERROR_AUTH_INCORRECT")) @@ -343,7 +364,10 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav $scope.tokenInParams = true $scope.data.token = $params.token else - $scope.tokenInParams = false + $location.path($navUrls.resolve("login")) + + text = $translate.instant("CHANGE_PASSWORD_RECOVERY_FORM.ERROR") + $confirm.notify("light-error",text) form = $el.find("form").checksley() @@ -354,7 +378,7 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav $confirm.success(text) onErrorSubmit = (response) -> - text = $translate.instant("COMMON.GENERIC_ERROR", {error: response.data._error_message}) + text = $translate.instant("CHANGE_PASSWORD_RECOVERY_FORM.ERROR") $confirm.notify("light-error", text) submit = debounce 2000, (event) => @@ -394,7 +418,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics $location.path($navUrls.resolve("login")) text = $translate.instant("INVITATION_LOGIN_FORM.NOT_FOUND") - $confirm.success(text) + $confirm.notify("light-error", text) # Login form $scope.dataLogin = {token: token} diff --git a/app/coffee/modules/backlog.coffee b/app/coffee/modules/backlog.coffee index f2c769f0..218f58aa 100644 --- a/app/coffee/modules/backlog.coffee +++ b/app/coffee/modules/backlog.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/backlog/filters.coffee b/app/coffee/modules/backlog/filters.coffee index bf6a4186..24f353c4 100644 --- a/app/coffee/modules/backlog/filters.coffee +++ b/app/coffee/modules/backlog/filters.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -35,11 +35,13 @@ module = angular.module("taigaBacklog") ## Issues Filters Directive ############################################################################# -BacklogFiltersDirective = ($log, $location, $templates) -> +BacklogFiltersDirective = ($q, $log, $location, $templates) -> template = $templates.get("backlog/filters.html", true) templateSelected = $templates.get("backlog/filter-selected.html", true) link = ($scope, $el, $attrs) -> + currentFiltersType = '' + $ctrl = $el.closest(".wrapper").controller() selectedFilters = [] @@ -50,16 +52,18 @@ BacklogFiltersDirective = ($log, $location, $templates) -> $el.find("h2 a.subfilter span.title").html(title) $el.find("h2 a.subfilter span.title").prop("data-type", type) + currentFiltersType = getFiltersType() + showCategories = -> $el.find(".filters-cats").show() $el.find(".filter-list").addClass("hidden") $el.find("h2.breadcrumb").addClass("hidden") - initializeSelectedFilters = (filters) -> + initializeSelectedFilters = () -> showCategories() selectedFilters = [] - for name, values of filters + for name, values of $scope.filters for val in values selectedFilters.push(val) if val.selected @@ -81,43 +85,62 @@ BacklogFiltersDirective = ($log, $location, $templates) -> html = template({filters:filters}) $el.find(".filter-list").html(html) + getFiltersType = () -> + return $el.find("h2 a.subfilter span.title").prop('data-type') + + reloadUserstories = () -> + currentFiltersType = getFiltersType() + + $q.all([$ctrl.loadUserstories(), $ctrl.generateFilters()]).then () -> + currentFilters = $scope.filters[currentFiltersType] + renderFilters(_.reject(currentFilters, "selected")) + toggleFilterSelection = (type, id) -> + currentFiltersType = getFiltersType() + filters = $scope.filters[type] - filter = _.find(filters, {id: taiga.toString(id)}) + filter = _.find(filters, {id: id}) filter.selected = (not filter.selected) + if filter.selected selectedFilters.push(filter) $scope.$apply -> $ctrl.selectFilter(type, id) else - selectedFilters = _.reject(selectedFilters, filter) - $scope.$apply -> - $ctrl.unselectFilter(type, id) + selectedFilters = _.reject selectedFilters, (selected) -> + return filter.type == selected.type && filter.id == selected.id + + $ctrl.unselectFilter(type, id) renderSelectedFilters(selectedFilters) - currentFiltersType = $el.find("h2 a.subfilter span.title").prop('data-type') if type == currentFiltersType renderFilters(_.reject(filters, "selected")) - $ctrl.loadUserstories() + reloadUserstories() selectQFilter = debounceLeading 100, (value) -> return if value is undefined + if value.length == 0 $ctrl.replaceFilter("q", null) else $ctrl.replaceFilter("q", value) - $ctrl.loadUserstories() + + reloadUserstories() $scope.$watch("filtersQ", selectQFilter) ## Angular Watchers - $scope.$on "filters:loaded", (ctx, filters) -> - initializeSelectedFilters(filters) + $scope.$on "backlog:loaded", (ctx) -> + initializeSelectedFilters() - $scope.$on "filters:update", (ctx, filters) -> - renderFilters(filters) + $scope.$on "filters:update", (ctx) -> + $ctrl.generateFilters().then () -> + filters = $scope.filters[currentFiltersType] + + if currentFiltersType + renderFilters(_.reject(filters, "selected")) ## Dom Event Handlers $el.on "click", ".filters-cats > ul > li > a", (event) -> @@ -126,7 +149,7 @@ BacklogFiltersDirective = ($log, $location, $templates) -> tags = $scope.filters[target.data("type")] renderFilters(_.reject(tags, "selected")) - showFilters(target.attr("title"), target.data("type")) + showFilters(target.attr("title"), target.data('type')) $el.on "click", ".filters-inner > .filters-step-cat > .breadcrumb > .back", (event) -> event.preventDefault() @@ -153,4 +176,4 @@ BacklogFiltersDirective = ($log, $location, $templates) -> return {link:link} -module.directive("tgBacklogFilters", ["$log", "$tgLocation", "$tgTemplate", BacklogFiltersDirective]) +module.directive("tgBacklogFilters", ["$q", "$log", "$tgLocation", "$tgTemplate", BacklogFiltersDirective]) diff --git a/app/coffee/modules/backlog/lightboxes.coffee b/app/coffee/modules/backlog/lightboxes.coffee index 57a91fd2..7de5eaed 100644 --- a/app/coffee/modules/backlog/lightboxes.coffee +++ b/app/coffee/modules/backlog/lightboxes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -34,12 +34,13 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading, hasErrors = false createSprint = true - $scope.sprint = { - project: null - name: null - estimated_start: null - estimated_finish: null - } + resetSprint = () -> + $scope.sprint = { + project: null + name: null + estimated_start: null + estimated_finish: null + } submit = debounce 2000, (event) => event.preventDefault() @@ -95,19 +96,30 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading, title = $translate.instant("LIGHTBOX.DELETE_SPRINT.TITLE") message = $scope.sprint.name - $confirm.askOnDelete(title, message).then (finish) => + $confirm.askOnDelete(title, message).then (askResponse) => onSuccess = -> - finish() + askResponse.finish() $scope.milestonesCounter -= 1 lightboxService.close($el) - $rootscope.$broadcast("sprintform:remove:success") + $rootscope.$broadcast("sprintform:remove:success", $scope.sprint) onError = -> - finish(false) + askResponse.finish(false) $confirm.notify("error") $repo.remove($scope.sprint).then(onSuccess, onError) + getLastSprint = -> + openSprints = _.filter $scope.sprints, (sprint) -> + return !sprint.closed + + sortedSprints = _.sortBy openSprints, (sprint) -> + return moment(sprint.estimated_finish, 'YYYY-MM-DD').format('X') + + return sortedSprints[sortedSprints.length - 1] + $scope.$on "sprintform:create", (event, projectId) -> + resetSprint() + form = $el.find("form").checksley() form.reset() @@ -117,20 +129,24 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading, $scope.sprint.name = null $scope.sprint.slug = null - lastSprint = $scope.sprints[0] + lastSprint = getLastSprint() estimatedStart = moment() - if $scope.sprint.estimated_start - estimatedStart = moment($scope.sprint.estimated_start) - else if lastSprint? + + if lastSprint estimatedStart = moment(lastSprint.estimated_finish) + else if $scope.sprint.estimated_start + estimatedStart = moment($scope.sprint.estimated_start) + $scope.sprint.estimated_start = estimatedStart.format(prettyDate) estimatedFinish = moment().add(2, "weeks") - if $scope.sprint.estimated_finish - estimatedFinish = moment($scope.sprint.estimated_finish) - else if lastSprint? + + if lastSprint estimatedFinish = moment(lastSprint.estimated_finish).add(2, "weeks") + else if $scope.sprint.estimated_finish + estimatedFinish = moment($scope.sprint.estimated_finish) + $scope.sprint.estimated_finish = estimatedFinish.format(prettyDate) lastSprintNameDom = $el.find(".last-sprint-name") @@ -152,6 +168,8 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading, $el.find(".last-sprint-name").removeClass("disappear") $scope.$on "sprintform:edit", (ctx, sprint) -> + resetSprint() + createSprint = false prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT") @@ -187,6 +205,8 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading, $scope.$on "$destroy", -> $el.off() + resetSprint() + return {link: link} diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee index d31dd548..47ec3d73 100644 --- a/app/coffee/modules/backlog/main.coffee +++ b/app/coffee/modules/backlog/main.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -28,6 +28,7 @@ bindOnce = @.taiga.bindOnce groupBy = @.taiga.groupBy timeout = @.taiga.timeout bindMethods = @.taiga.bindMethods +generateHash = @.taiga.generateHash module = angular.module("taigaBacklog") @@ -60,6 +61,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME") @showTags = false @activeFilters = false + @scope.showGraphPlaceholder = null @.initializeEventHandlers() @@ -96,18 +98,26 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @scope.$on "usform:new:success", => @.loadUserstories() @.loadProjectStats() + + @rootscope.$broadcast("filters:update") @analytics.trackEvent("userstory", "create", "create userstory on backlog", 1) @scope.$on "sprintform:edit:success", => @.loadProjectStats() - @scope.$on "sprintform:remove:success", => + @scope.$on "sprintform:remove:success", (event, sprint) => @.loadSprints() @.loadProjectStats() @.loadUserstories() + if sprint.closed + @.loadClosedSprints() + + @rootscope.$broadcast("filters:update") + @scope.$on "usform:edit:success", => @.loadUserstories() + @rootscope.$broadcast("filters:update") @scope.$on("sprint:us:move", @.moveUs) @scope.$on("sprint:us:moved", @.loadSprints) @@ -137,18 +147,16 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F loadProjectStats: -> return @rs.projects.stats(@scope.projectId).then (stats) => @scope.stats = stats + totalPoints = if stats.total_points then stats.total_points else stats.defined_points - if stats.total_points - @scope.stats.completedPercentage = Math.round(100 * stats.closed_points / stats.total_points) + if totalPoints + @scope.stats.completedPercentage = Math.round(100 * stats.closed_points / totalPoints) else @scope.stats.completedPercentage = 0 + @scope.showGraphPlaceholder = !(stats.total_points? && stats.total_milestones?) return stats - refreshTagsColors: -> - return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => - @scope.project.tags_colors = tags_colors - unloadClosedSprints: -> @scope.$apply => @scope.closedSprints = [] @@ -156,17 +164,29 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F loadClosedSprints: -> params = {closed: true} - return @rs.sprints.list(@scope.projectId, params).then (sprints) => + return @rs.sprints.list(@scope.projectId, params).then (result) => + sprints = result.milestones + + @scope.totalClosedMilestones = result.closed + # NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files for sprint in sprints sprint.user_stories = _.sortBy(sprint.user_stories, "sprint_order") @scope.closedSprints = sprints + @scope.closedSprintsById = groupBy(sprints, (x) -> x.id) @rootscope.$broadcast("closed-sprints:reloaded", sprints) return sprints loadSprints: -> params = {closed: false} - return @rs.sprints.list(@scope.projectId, params).then (sprints) => + return @rs.sprints.list(@scope.projectId, params).then (result) => + sprints = result.milestones + + @scope.totalMilestones = sprints + @scope.totalClosedMilestones = result.closed + @scope.totalOpenMilestones = result.open + @scope.totalMilestones = @scope.totalOpenMilestones + @scope.totalClosedMilestones + # NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files for sprint in sprints sprint.user_stories = _.sortBy(sprint.user_stories, "sprint_order") @@ -182,7 +202,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F resetFilters: -> selectedTags = _.filter(@scope.filters.tags, "selected") - selectedStatuses = _.filter(@scope.filters.statuses, "selected") + selectedStatuses = _.filter(@scope.filters.status, "selected") @scope.filtersQ = "" @@ -195,23 +215,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @.unselectFilter(item.type, item.id) @.loadUserstories() + @rootscope.$broadcast("filters:update") loadUserstories: -> @scope.httpParams = @.getUrlFilters() @rs.userstories.storeQueryParams(@scope.projectId, @scope.httpParams) - promise = @q.all([@.refreshTagsColors(), @rs.userstories.listUnassigned(@scope.projectId, @scope.httpParams)]) + promise = @rs.userstories.listUnassigned(@scope.projectId, @scope.httpParams) - return promise.then (data) => - userstories = data[1] + return promise.then (userstories) => # NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files @scope.userstories = _.sortBy(userstories, "backlog_order") @.setSearchDataFilters() - @.filterVisibleUserstories() - @.generateFilters() - @rootscope.$broadcast("filters:loaded", @scope.filters) # The broadcast must be executed when the DOM has been fully reloaded. # We can't assure when this exactly happens so we need a defer scopeDefer @scope, => @@ -233,7 +250,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @scope.projectId = project.id @scope.project = project - @scope.totalClosedMilestones = project.total_closed_milestones + @scope.closedMilestones = !!project.total_closed_milestones @scope.$emit('project:loaded', project) @scope.points = _.sortBy(project.points, "order") @scope.pointsById = groupBy(project.points, (x) -> x.id) @@ -244,25 +261,13 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F loadInitialData: -> promise = @.loadProject() promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @.initializeSubscription() - return promise.then(=> @.loadBacklog()) - - filterVisibleUserstories: -> - @scope.visibleUserstories = [] - - # Filter by tags - @scope.visibleUserstories = _.reject @scope.userstories, (us) => - return _.some us.tags, (tag) => - return @isFilterSelected("tag", tag) - - # Filter by status - @scope.visibleUserstories = _.filter @scope.visibleUserstories, (us) => - if @searchdata["statuses"] && Object.keys(@searchdata["statuses"]).length - return @isFilterSelected("statuses", taiga.toString(us.status)) - - return true + return promise + .then(=> @.loadBacklog()) + .then(=> @.generateFilters()) + .then(=> @scope.$emit("backlog:loaded")) prepareBulkUpdateData: (uses, field="backlog_order") -> return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) @@ -281,6 +286,23 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F oldSprintId = usList[0].milestone project = usList[0].project + movedFromClosedSprint = false + movedToClosedSprint = false + + sprint = @scope.sprintsById[oldSprintId] + + # Move from closed sprint + if !sprint && @scope.closedSprintsById + sprint = @scope.closedSprintsById[oldSprintId] + movedFromClosedSprint = true if sprint + + newSprint = @scope.sprintsById[newSprintId] + + # Move to closed sprint + if !newSprint && newSprintId + newSprint = @scope.closedSprintsById[newSprintId] + movedToClosedSprint = true if newSprint + # In the same sprint or in the backlog if newSprintId == oldSprintId items = null @@ -289,7 +311,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F if newSprintId == null userstories = @scope.userstories else - userstories = @scope.sprintsById[newSprintId].user_stories + userstories = newSprint.user_stories @scope.$apply -> for us, key in usList @@ -333,16 +355,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @scope.$apply => # Add new us to backlog userstories list # @scope.userstories.splice(newUsIndex, 0, us) - # @scope.visibleUserstories.splice(newUsIndex, 0, us) args = [newUsIndex, 0].concat(usList) Array.prototype.splice.apply(@scope.userstories, args) - Array.prototype.splice.apply(@scope.visibleUserstories, args) - # Execute the prefiltering of user stories - @.filterVisibleUserstories() - - # Remove the us from the sprint list. - sprint = @scope.sprintsById[oldSprintId] for us, key in usList r = sprint.user_stories.indexOf(us) sprint.user_stories.splice(r, 1) @@ -358,13 +373,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F return @rs.userstories.bulkUpdateBacklogOrder(us.project, data).then => @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) + if movedFromClosedSprint + @rootscope.$broadcast("backlog:load-closed-sprints") + promise.then null, -> console.log "FAIL" # TODO return promise # From backlog to sprint - newSprint = @scope.sprintsById[newSprintId] if oldSprintId == null us.milestone = newSprintId for us in usList @@ -376,9 +393,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # Remove moving us from backlog userstories lists. for us, key in usList - r = @scope.visibleUserstories.indexOf(us) - @scope.visibleUserstories.splice(r, 1) - r = @scope.userstories.indexOf(us) @scope.userstories.splice(r, 1) @@ -394,9 +408,8 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # Remove the us from the sprint list. for us in usList - oldSprint = @scope.sprintsById[oldSprintId] - r = oldSprint.user_stories.indexOf(us) - oldSprint.user_stories.splice(r, 1) + r = sprint.user_stories.indexOf(us) + sprint.user_stories.splice(r, 1) # Persist the milestone change of userstory promises = _.map usList, (us) => @repo.save(us) @@ -407,13 +420,16 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F items = @.resortUserStories(newSprint.user_stories, "sprint_order") data = @.prepareBulkUpdateData(items, "sprint_order") - @rs.userstories.bulkUpdateSprintOrder(project, data).then => + @rs.userstories.bulkUpdateSprintOrder(project, data).then (result) => @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) @rs.userstories.bulkUpdateBacklogOrder(project, data).then => for us in usList @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) + if movedToClosedSprint || movedFromClosedSprint + @scope.$broadcast("backlog:load-closed-sprints") + promise.then null, -> console.log "FAIL" # TODO @@ -439,74 +455,77 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F @searchdata[name][val] = true getUrlFilters: -> - return _.pick(@location.search(), "statuses", "tags", "q") + return _.pick(@location.search(), "status", "tags", "q") generateFilters: -> urlfilters = @.getUrlFilters() - @scope.filters = {} + @scope.filters = {} - #tags - plainTags = _.flatten(_.filter(_.map(@scope.visibleUserstories, "tags"))) - plainTags.sort() + loadFilters = {} + loadFilters.project = @scope.projectId + loadFilters.tags = urlfilters.tags + loadFilters.status = urlfilters.status + loadFilters.q = urlfilters.q + loadFilters.milestone = 'null' - if plainTags.length == 0 and urlfilters["tags"] - plainTags.push(urlfilters["tags"]) + return @rs.userstories.filtersData(loadFilters).then (data) => + choicesFiltersFormat = (choices, type, byIdObject) => + _.map choices, (t) -> + t.type = type + return t - @scope.filters.tags = _.map _.countBy(plainTags), (v, k) => - obj = { - id: k, - type: "tags", - name: k, - color: @scope.project.tags_colors[k], - count: v - } - obj.selected = true if @isFilterSelected("tags", obj.id) - return obj + tagsFilterFormat = (tags) => + return _.map tags, (t) -> + t.id = t.name + t.type = 'tags' + return t - selectedTags = _.filter(@scope.filters.tags, "selected") - selectedTags = _.map(selectedTags, "name") + # Build filters data structure + @scope.filters.status = choicesFiltersFormat(data.statuses, "status", @scope.usStatusById) + @scope.filters.tags = tagsFilterFormat(data.tags) - #status - plainStatuses = _.map(@scope.visibleUserstories, "status") + selectedTags = _.filter(@scope.filters.tags, "selected") + selectedTags = _.map(selectedTags, "id") - plainStatuses = _.filter plainStatuses, (status) => - if status - return status + selectedStatuses = _.filter(@scope.filters.status, "selected") + selectedStatuses = _.map(selectedStatuses, "id") - if plainStatuses.length == 0 and urlfilters["statuses"] - plainStatuses.push(urlfilters["statuses"]) + @.markSelectedFilters(@scope.filters, urlfilters) - @scope.filters.statuses = _.map _.countBy(plainStatuses), (v, k) => - obj = { - id: k, - type: "statuses", - name: @scope.usStatusById[k].name, - color: @scope.usStatusById[k].color, - count:v - } - obj.selected = true if @isFilterSelected("statuses", obj.id) + #store query params + @rs.userstories.storeQueryParams(@scope.projectId, { + "status": selectedStatuses, + "tags": selectedTags, + "project": @scope.projectId + "milestone": null + }) - return obj + markSelectedFilters: (filters, urlfilters) -> + # Build selected filters (from url) fast lookup data structure + searchdata = {} + for name, value of _.omit(urlfilters, "page", "orderBy") + if not searchdata[name]? + searchdata[name] = {} - selectedStatuses = _.filter(@scope.filters.statuses, "selected") - selectedStatuses = _.map(selectedStatuses, "id") + for val in "#{value}".split(",") + searchdata[name][val] = true - #store query params - @rs.userstories.storeQueryParams(@scope.projectId, { - "status": selectedStatuses, - "tags": selectedTags, - "project": @scope.projectId - "milestone": null - }) + isSelected = (type, id) -> + if searchdata[type]? and searchdata[type][id] + return true + return false + + for key, value of filters + for obj in value + obj.selected = if isSelected(obj.type, obj.id) then true else undefined ## Template actions updateUserStoryStatus: () -> @.setSearchDataFilters() - @.filterVisibleUserstories() - @.generateFilters() - @rootscope.$broadcast("filters:update", @scope.filters['statuses']) - @.loadProjectStats() + @.generateFilters().then () => + @rootscope.$broadcast("filters:update") + @.loadProjectStats() editUserStory: (projectId, ref, $event) -> target = $($event.target) @@ -527,16 +546,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F message = us.subject - @confirm.askOnDelete(title, message).then (finish) => + @confirm.askOnDelete(title, message).then (askResponse) => # We modify the userstories in scope so the user doesn't see the removed US for a while @scope.userstories = _.without(@scope.userstories, us) - @filterVisibleUserstories() promise = @.repo.remove(us) promise.then => - finish() + askResponse.finish() @.loadBacklog() promise.then null, => - finish(false) + askResponse.finish(false) @confirm.notify("error") addNewUs: (type) -> @@ -563,7 +581,7 @@ BacklogDirective = ($repo, $rootscope, $translate) -> linkDoomLine = ($scope, $el, $attrs, $ctrl) -> reloadDoomLine = -> - if $scope.stats? + if $scope.stats? and $scope.stats.total_points? and $scope.stats.total_points != 0 removeDoomlineDom() stats = $scope.stats @@ -571,9 +589,9 @@ BacklogDirective = ($repo, $rootscope, $translate) -> total_points = stats.total_points current_sum = stats.assigned_points - return if not $scope.visibleUserstories + return if not $scope.userstories - for us, i in $scope.visibleUserstories + for us, i in $scope.userstories current_sum += us.total_points if current_sum > total_points @@ -614,7 +632,6 @@ BacklogDirective = ($repo, $rootscope, $translate) -> # Update the total of points $scope.sprints[0].total_points += totalExtraPoints - $ctrl.filterVisibleUserstories() $repo.saveAll(selectedUss).then -> $ctrl.loadSprints() $ctrl.loadProjectStats() @@ -626,7 +643,7 @@ BacklogDirective = ($repo, $rootscope, $translate) -> checkSelected = (target) -> lastChecked = target.closest(".us-item-row") moveToCurrentSprintDom = $el.find("#move-to-current-sprint") - selectedUsDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") + selectedUsDom = $el.find(".backlog-table-body input:checkbox:checked") if selectedUsDom.length > 0 and $scope.sprints.length > 0 moveToCurrentSprintDom.show() @@ -641,7 +658,7 @@ BacklogDirective = ($repo, $rootscope, $translate) -> return true # Enable move to current sprint only when there are selected us's - $el.on "change", ".backlog-table-body .user-stories input:checkbox", (event) -> + $el.on "change", ".backlog-table-body input:checkbox", (event) -> # check elements between the last two if shift is pressed if lastChecked && shiftPressed elements = [] @@ -656,15 +673,16 @@ BacklogDirective = ($repo, $rootscope, $translate) -> _.map elements, (elm) -> input = $(elm).find("input:checkbox") - input.prop('checked', true); + input.prop('checked', true) checkSelected(input) target = angular.element(event.currentTarget) + target.closest(".us-item-row").toggleClass('is-checked') checkSelected(target) $el.on "click", "#move-to-current-sprint", (event) => # Calculating the us's to be modified - ussDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") + ussDom = $el.find(".backlog-table-body input:checkbox:checked") ussToMove = _.map ussDom, (item) -> item = $(item).closest('.tg-scope') @@ -688,12 +706,12 @@ BacklogDirective = ($repo, $rootscope, $translate) -> elm.addClass("active") text = $translate.instant("BACKLOG.TAGS.HIDE") - elm.find(".text").text(text) + elm.text(text) else elm.removeClass("active") text = $translate.instant("BACKLOG.TAGS.SHOW") - elm.find(".text").text(text) + elm.text(text) showHideFilter = ($scope, $el, $ctrl) -> sidebar = $el.find("sidebar.filters-bar") @@ -736,8 +754,7 @@ BacklogDirective = ($repo, $rootscope, $translate) -> $el.find(".backlog-table-body").disableSelection() filters = $ctrl.getUrlFilters() - - if filters.statuses || + if filters.status || filters.tags || filters.q showHideFilter($scope, $el, $ctrl) @@ -915,6 +932,56 @@ UsPointsDirective = ($tgEstimationsService, $repo, $tgTemplate) -> module.directive("tgBacklogUsPoints", ["$tgEstimationsService", "$tgRepo", "$tgTemplate", UsPointsDirective]) +############################################################################# +## Burndown graph directive +############################################################################# +ToggleBurndownVisibility = ($storage) -> + hide = () -> + $(".js-burndown-graph").removeClass("shown") + $(".js-toggle-burndown-visibility-button").removeClass("active") + $(".js-burndown-graph").removeClass("open") + + show = (firstLoad) -> + $(".js-toggle-burndown-visibility-button").addClass("active") + + if firstLoad + $(".js-burndown-graph").addClass("shown") + else + $(".js-burndown-graph").addClass("open") + + link = ($scope, $el, $attrs) -> + firstLoad = true + hash = generateHash(["is-burndown-grpahs-collapsed"]) + $scope.isBurndownGraphCollapsed = $storage.get(hash) or false + + toggleGraph = -> + if $scope.isBurndownGraphCollapsed + hide(firstLoad) + else + show(firstLoad) + + firstLoad = false + + $scope.$watch "showGraphPlaceholder", () -> + if $scope.showGraphPlaceholder? + $scope.isBurndownGraphCollapsed = $scope.isBurndownGraphCollapsed || $scope.showGraphPlaceholder + toggleGraph() + + $el.on "click", ".js-toggle-burndown-visibility-button", -> + $scope.isBurndownGraphCollapsed = !$scope.isBurndownGraphCollapsed + $storage.set(hash, $scope.isBurndownGraphCollapsed) + toggleGraph() + + $scope.$on "$destroy", -> + $el.off() + + return { + link: link + } + +module.directive("tgToggleBurndownVisibility", ["$tgStorage", ToggleBurndownVisibility]) + + ############################################################################# ## Burndown graph directive ############################################################################# @@ -1007,16 +1074,16 @@ BurndownBacklogGraphDirective = ($translate) -> tooltipOpts: { content: (label, xval, yval, flotItem) -> if flotItem.seriesIndex == 1 - ctx = {xval: xval, yval: yval} + ctx = {sprintName: dataToDraw.milestones[xval].name, value: Math.abs(yval)} return $translate.instant("BACKLOG.CHART.OPTIMAL", ctx) else if flotItem.seriesIndex == 2 - ctx = {xval: xval, yval: yval} + ctx = {sprintName: dataToDraw.milestones[xval].name, value: Math.abs(yval)} return $translate.instant("BACKLOG.CHART.REAL", ctx) else if flotItem.seriesIndex == 3 - ctx = {xval: xval, yval: Math.abs(yval)} + ctx = {sprintName: dataToDraw.milestones[xval].name, value: Math.abs(yval)} return $translate.instant("BACKLOG.CHART.INCREMENT_TEAM", ctx) else - ctx = {xval: xval, yval: Math.abs(yval)} + ctx = {sprintName: dataToDraw.milestones[xval].name, value: Math.abs(yval)} return $translate.instant("BACKLOG.CHART.INCREMENT_CLIENT", ctx) } } @@ -1046,14 +1113,16 @@ module.directive("tgBurndownBacklogGraph", ["$translate", BurndownBacklogGraphDi ## Backlog progress bar directive ############################################################################# -TgBacklogProgressBarDirective = ($template) -> +TgBacklogProgressBarDirective = ($template, $compile) -> template = $template.get("backlog/progress-bar.html", true) - render = (el, projectPointsPercentaje, closedPointsPercentaje) -> - el.html(template({ + render = (scope, el, projectPointsPercentaje, closedPointsPercentaje) -> + html = template({ projectPointsPercentaje: projectPointsPercentaje, closedPointsPercentaje:closedPointsPercentaje - })) + }) + html = $compile(html)(scope) + el.html(html) adjustPercentaje = (percentage) -> adjusted = _.max([0 , percentage]) @@ -1065,7 +1134,7 @@ TgBacklogProgressBarDirective = ($template) -> $scope.$watch $attrs.tgBacklogProgressBar, (stats) -> if stats? - totalPoints = stats.total_points + totalPoints = if stats.total_points then stats.total_points else stats.defined_points definedPoints = stats.defined_points closedPoints = stats.closed_points if definedPoints > totalPoints @@ -1077,11 +1146,11 @@ TgBacklogProgressBarDirective = ($template) -> projectPointsPercentaje = adjustPercentaje(projectPointsPercentaje - 3) closedPointsPercentaje = adjustPercentaje(closedPointsPercentaje - 3) - render($el, projectPointsPercentaje, closedPointsPercentaje) + render($scope, $el, projectPointsPercentaje, closedPointsPercentaje) $scope.$on "$destroy", -> $el.off() return {link: link} -module.directive("tgBacklogProgressBar", ["$tgTemplate", TgBacklogProgressBarDirective]) +module.directive("tgBacklogProgressBar", ["$tgTemplate", "$compile", TgBacklogProgressBarDirective]) diff --git a/app/coffee/modules/backlog/sortable.coffee b/app/coffee/modules/backlog/sortable.coffee index 70b25dbf..af75929f 100644 --- a/app/coffee/modules/backlog/sortable.coffee +++ b/app/coffee/modules/backlog/sortable.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -144,6 +144,7 @@ BacklogEmptySortableDirective = ($repo, $rs, $rootscope) -> # If the user has not enough permissions we don't enable the sortable if project.my_permissions.indexOf("modify_us") > -1 $el.sortable({ + items: ".us-item-row", dropOnEmpty: true }) diff --git a/app/coffee/modules/backlog/sprints.coffee b/app/coffee/modules/backlog/sprints.coffee index 4db13c68..12162fcc 100644 --- a/app/coffee/modules/backlog/sprints.coffee +++ b/app/coffee/modules/backlog/sprints.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -35,12 +35,6 @@ BacklogSprintDirective = ($repo, $rootscope) -> easing: 'linear' } - refreshSprintTableHeight = (sprintTable) => - if !sprintTable.find(".row").length - sprintTable.css("height", sprintTableMinHeight) - else - sprintTable.css("height", "auto") - toggleSprint = ($el) => sprintTable = $el.find(".sprint-table") sprintArrow = $el.find(".icon-arrow-up") @@ -48,8 +42,6 @@ BacklogSprintDirective = ($repo, $rootscope) -> sprintArrow.toggleClass('active') sprintTable.toggleClass('open') - refreshSprintTableHeight(sprintTable) - link = ($scope, $el, $attrs) -> $scope.$watch $attrs.tgBacklogSprint, (sprint) -> sprint = $scope.$eval($attrs.tgBacklogSprint) diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index dbd56c0e..f916a2b2 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/bind.coffee b/app/coffee/modules/base/bind.coffee index 59e8c92a..a54cfb58 100644 --- a/app/coffee/modules/base/bind.coffee +++ b/app/coffee/modules/base/bind.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/conf.coffee b/app/coffee/modules/base/conf.coffee index ad3ad0d7..f5bfa967 100644 --- a/app/coffee/modules/base/conf.coffee +++ b/app/coffee/modules/base/conf.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/contrib.coffee b/app/coffee/modules/base/contrib.coffee index 8cb17162..9f39cef4 100644 --- a/app/coffee/modules/base/contrib.coffee +++ b/app/coffee/modules/base/contrib.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/filters.coffee b/app/coffee/modules/base/filters.coffee index 8cb68479..3a094814 100644 --- a/app/coffee/modules/base/filters.coffee +++ b/app/coffee/modules/base/filters.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/http.coffee b/app/coffee/modules/base/http.coffee index 44647e6b..e6c9dc03 100644 --- a/app/coffee/modules/base/http.coffee +++ b/app/coffee/modules/base/http.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/location.coffee b/app/coffee/modules/base/location.coffee index 8e05f02f..9ac46e6b 100644 --- a/app/coffee/modules/base/location.coffee +++ b/app/coffee/modules/base/location.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/model.coffee b/app/coffee/modules/base/model.coffee index 20bd5022..b4787dea 100644 --- a/app/coffee/modules/base/model.coffee +++ b/app/coffee/modules/base/model.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/navurls.coffee b/app/coffee/modules/base/navurls.coffee index d3908790..d95d52a5 100644 --- a/app/coffee/modules/base/navurls.coffee +++ b/app/coffee/modules/base/navurls.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -71,16 +71,39 @@ NavigationUrlsDirective = ($navurls, $auth, $q, $location) -> parseNav = (data, $scope) -> [name, params] = _.map(data.split(":"), trim) if params - params = _.map(params.split(","), trim) + # split by 'xxx=' + # example + # project=vm.timeline.getIn(['data', 'project', 'slug']), ref=vm.timeline.getIn(['obj', 'ref']) + # ["", "project", "vm.timeline.getIn(['data', 'project', 'slug']), ", "ref", "vm.timeline.getIn(['obj', 'ref'])"] + result = params.split(/(\w+)=/) + + # remove empty string + result = _.filter result, (str) -> return str.length + + # remove , at the end of the string + result = _.map result, (str) -> return trim(str.replace(/,$/g, '')) + + params = [] + index = 0 + + # ['param1', 'value'] => [{'param1': 'value'}] + while index < result.length + obj = {} + obj[result[index]] = result[index + 1] + params.push obj + index = index + 2 else params = [] - values = _.map(params, (x) -> trim(x.split("=")[1])) + + values = _.map params, (param) -> _.values(param)[0] promises = _.map(values, (x) -> bindOnceP($scope, x)) return $q.all(promises).then -> options = {} - for item in params - [key, value] = _.map(item.split("="), trim) + for param in params + key = Object.keys(param)[0] + value = param[key] + options[key] = $scope.$eval(value) return [name, options] diff --git a/app/coffee/modules/base/repository.coffee b/app/coffee/modules/base/repository.coffee index 0089ad3d..7707dfc1 100644 --- a/app/coffee/modules/base/repository.coffee +++ b/app/coffee/modules/base/repository.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -140,7 +140,7 @@ class RepositoryService extends taiga.Service return defered.promise - queryMany: (name, params, options={}) -> + queryMany: (name, params, options={}, headers=false) -> url = @urls.resolve(name) httpOptions = {headers: {}} @@ -148,7 +148,12 @@ class RepositoryService extends taiga.Service httpOptions.headers["x-disable-pagination"] = "1" return @http.get(url, params, httpOptions).then (data) => - return _.map(data.data, (x) => @model.make_model(name, x)) + result = _.map(data.data, (x) => @model.make_model(name, x)) + + if headers + return [result, data.headers] + + return result queryOneAttribute: (name, id, attribute, params, options={}) -> url = @urls.resolve(name, id) diff --git a/app/coffee/modules/base/storage.coffee b/app/coffee/modules/base/storage.coffee index 2eafb2f3..c3cfb968 100644 --- a/app/coffee/modules/base/storage.coffee +++ b/app/coffee/modules/base/storage.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/base/urls.coffee b/app/coffee/modules/base/urls.coffee index 46339b68..e7843959 100644 --- a/app/coffee/modules/base/urls.coffee +++ b/app/coffee/modules/base/urls.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/common.coffee b/app/coffee/modules/common.coffee index abe17a6c..268242d3 100644 --- a/app/coffee/modules/common.coffee +++ b/app/coffee/modules/common.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -23,6 +23,57 @@ taiga = @.taiga module = angular.module("taigaCommon", []) +############################################################################# +## Default datepicker config +############################################################################# +DataPickerConfig = ($translate) -> + return { + get: () -> + return { + i18n: { + previousMonth: $translate.instant("COMMON.PICKERDATE.PREV_MONTH"), + nextMonth: $translate.instant("COMMON.PICKERDATE.NEXT_MONTH"), + months: [ + $translate.instant("COMMON.PICKERDATE.MONTHS.JAN"), + $translate.instant("COMMON.PICKERDATE.MONTHS.FEB"), + $translate.instant("COMMON.PICKERDATE.MONTHS.MAR"), + $translate.instant("COMMON.PICKERDATE.MONTHS.APR"), + $translate.instant("COMMON.PICKERDATE.MONTHS.MAY"), + $translate.instant("COMMON.PICKERDATE.MONTHS.JUN"), + $translate.instant("COMMON.PICKERDATE.MONTHS.JUL"), + $translate.instant("COMMON.PICKERDATE.MONTHS.AUG"), + $translate.instant("COMMON.PICKERDATE.MONTHS.SEP"), + $translate.instant("COMMON.PICKERDATE.MONTHS.OCT"), + $translate.instant("COMMON.PICKERDATE.MONTHS.NOV"), + $translate.instant("COMMON.PICKERDATE.MONTHS.DEC") + ], + weekdays: [ + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.SUN"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.MON"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.TUE"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.WED"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.THU"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.FRI"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.SAT") + ], + weekdaysShort: [ + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.SUN"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.MON"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.TUE"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.WED"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.THU"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.FRI"), + $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.SAT") + ] + }, + isRTL: $translate.instant("COMMON.PICKERDATE.IS_RTL") == "true", + firstDay: parseInt($translate.instant("COMMON.PICKERDATE.FIRST_DAY_OF_WEEK"), 10), + format: $translate.instant("COMMON.PICKERDATE.FORMAT") + } + } + +module.factory("tgDatePickerConfigService", ["$translate", DataPickerConfig]) + ############################################################################# ## Get the selected text ############################################################################# @@ -229,3 +280,46 @@ Template = ($templateCache) -> } module.factory("$tgTemplate", ["$templateCache", Template]) + +############################################################################# +## Permission directive, hide elements when necessary +############################################################################# + +Capslock = ($translate) -> + link = ($scope, $el, $attrs) -> + open = false + + warningIcon = $('
') + .addClass('icon') + .addClass('icon-capslock') + .attr('title', $translate.instant('COMMON.CAPSLOCK_WARNING')) + + hideIcon = () -> + warningIcon.fadeOut () -> + open = false + + $(this).remove() + + showIcon = (e) -> + return if open + element = e.currentTarget + $(element).parent().append(warningIcon) + $('.icon-capslock').fadeIn() + + open = true + + $el.on 'blur', (e) -> + hideIcon() + + $el.on 'keyup.capslock, focus', (e) -> + if $el.val() == $el.val().toLowerCase() + hideIcon(e) + else + showIcon(e) + + $scope.$on "$destroy", -> + $el.off('.capslock') + + return {link:link} + +module.directive("tgCapslock", ["$translate", Capslock]) diff --git a/app/coffee/modules/common/analytics.coffee b/app/coffee/modules/common/analytics.coffee index 703de053..ffad1347 100644 --- a/app/coffee/modules/common/analytics.coffee +++ b/app/coffee/modules/common/analytics.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/common/attachments.coffee b/app/coffee/modules/common/attachments.coffee index 6f269935..f06f19cf 100644 --- a/app/coffee/modules/common/attachments.coffee +++ b/app/coffee/modules/common/attachments.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -136,16 +136,16 @@ class AttachmentsController extends taiga.Controller title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT") message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", {fileName: attachment.name}) - return @confirm.askOnDelete(title, message).then (finish) => + return @confirm.askOnDelete(title, message).then (askResponse) => onSuccess = => - finish() + askResponse.finish() index = @.attachments.indexOf(attachment) @.attachments.splice(index, 1) @.updateCounters() @rootscope.$broadcast("attachment:delete") onError = => - finish(false) + askResponse.finish(false) message = @translate.instant("ATTACHMENT.ERROR_DELETE_ATTACHMENT", {errorMessage: message}) @confirm.notify("error", null, message) return @q.reject() @@ -197,6 +197,7 @@ AttachmentsDirective = ($config, $confirm, $templates, $translate) -> $el.on "change", ".attachments-header input", (event) -> files = _.toArray(event.target.files) + return if files.length < 1 $scope.$apply -> @@ -245,7 +246,7 @@ AttachmentsDirective = ($config, $confirm, $templates, $translate) -> module.directive("tgAttachments", ["$tgConfig", "$tgConfirm", "$tgTemplate", "$translate", AttachmentsDirective]) -AttachmentDirective = ($template, $compile, $translate) -> +AttachmentDirective = ($template, $compile, $translate, $rootScope) -> template = $template.get("attachment/attachment.html", true) templateEdit = $template.get("attachment/attachment-edit.html", true) @@ -283,6 +284,7 @@ AttachmentDirective = ($template, $compile, $translate) -> saveAttachment = -> attachment.description = $el.find("input[name='description']").val() attachment.is_deprecated = $el.find("input[name='is-deprecated']").prop("checked") + attachment.isCreatedRightNow = false $scope.$apply -> $ctrl.updateAttachment(attachment).then -> @@ -297,7 +299,7 @@ AttachmentDirective = ($template, $compile, $translate) -> if event.keyCode == 13 saveAttachment() else if event.keyCode == 27 - render(attachment, false) + $scope.$apply -> render(attachment, false) $el.on "click", "a.editable-settings.icon-delete", (event) -> event.preventDefault() @@ -314,6 +316,12 @@ AttachmentDirective = ($template, $compile, $translate) -> $scope.$apply -> $ctrl.removeAttachment(attachment) + $el.on "click", "div.attachment-name a", (event) -> + if null != attachment.name.match(/\.(jpe?g|png|gif|gifv|webm)/i) + event.preventDefault() + $scope.$apply -> + $rootScope.$broadcast("attachment:preview", attachment) + $scope.$on "$destroy", -> $el.off() @@ -329,4 +337,4 @@ AttachmentDirective = ($template, $compile, $translate) -> restrict: "AE" } -module.directive("tgAttachment", ["$tgTemplate", "$compile", "$translate", AttachmentDirective]) +module.directive("tgAttachment", ["$tgTemplate", "$compile", "$translate", "$rootScope", AttachmentDirective]) diff --git a/app/coffee/modules/common/bind-scope.coffee b/app/coffee/modules/common/bind-scope.coffee index 2278e4b2..afe6c636 100644 --- a/app/coffee/modules/common/bind-scope.coffee +++ b/app/coffee/modules/common/bind-scope.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: bind-scope.coffee +### + module = angular.module("taigaCommon") BindScope = (config) -> diff --git a/app/coffee/modules/common/compile-html.directive.coffee b/app/coffee/modules/common/compile-html.directive.coffee index 195d6a2c..df6bfcbd 100644 --- a/app/coffee/modules/common/compile-html.directive.coffee +++ b/app/coffee/modules/common/compile-html.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: compile-html.directive.coffee +### + CompileHtmlDirective = ($compile) -> link = (scope, element, attrs) -> scope.$watch attrs.tgCompileHtml, (newValue, oldValue) -> diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index c6e9a948..b71abdb4 100644 --- a/app/coffee/modules/common/components.coffee +++ b/app/coffee/modules/common/components.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -52,52 +52,23 @@ module.directive("tgDateRange", ["$translate", DateRangeDirective]) ## Date Selector Directive (using pikaday) ############################################################################# -DateSelectorDirective = ($rootscope, $translate) -> +DateSelectorDirective = ($rootscope, datePickerConfigService) -> link = ($scope, $el, $attrs, $model) -> selectedDate = null initialize = () -> - $el.picker = new Pikaday({ + datePickerConfig = datePickerConfigService.get() + + _.merge(datePickerConfig, { field: $el[0] onSelect: (date) => selectedDate = date onOpen: => $el.picker.setDate(selectedDate) if selectedDate? - i18n: { - previousMonth: $translate.instant("COMMON.PICKERDATE.PREV_MONTH"), - nextMonth: $translate.instant("COMMON.PICKERDATE.NEXT_MONTH"), - months: [$translate.instant("COMMON.PICKERDATE.MONTHS.JAN"), - $translate.instant("COMMON.PICKERDATE.MONTHS.FEB"), - $translate.instant("COMMON.PICKERDATE.MONTHS.MAR"), - $translate.instant("COMMON.PICKERDATE.MONTHS.APR"), - $translate.instant("COMMON.PICKERDATE.MONTHS.MAY"), - $translate.instant("COMMON.PICKERDATE.MONTHS.JUN"), - $translate.instant("COMMON.PICKERDATE.MONTHS.JUL"), - $translate.instant("COMMON.PICKERDATE.MONTHS.AUG"), - $translate.instant("COMMON.PICKERDATE.MONTHS.SEP"), - $translate.instant("COMMON.PICKERDATE.MONTHS.OCT"), - $translate.instant("COMMON.PICKERDATE.MONTHS.NOV"), - $translate.instant("COMMON.PICKERDATE.MONTHS.DEC")], - weekdays: [$translate.instant("COMMON.PICKERDATE.WEEK_DAYS.SUN"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.MON"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.TUE"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.WED"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.THU"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.FRI"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS.SAT")], - weekdaysShort: [$translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.SUN"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.MON"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.TUE"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.WED"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.THU"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.FRI"), - $translate.instant("COMMON.PICKERDATE.WEEK_DAYS_SHORT.SAT")] - }, - isRTL: $translate.instant("COMMON.PICKERDATE.IS_RTL") == "true", - firstDay: parseInt($translate.instant("COMMON.PICKERDATE.FIRST_DAY_OF_WEEK"), 10), - format: $translate.instant("COMMON.PICKERDATE.FORMAT") }) + $el.picker = new Pikaday(datePickerConfig) + unbind = $rootscope.$on "$translateChangeEnd", (ctx) => initialize() $scope.$watch $attrs.ngModel, (val) -> @@ -113,7 +84,7 @@ DateSelectorDirective = ($rootscope, $translate) -> require: "ngModel" } -module.directive("tgDateSelector", ["$rootScope", "$translate", DateSelectorDirective]) +module.directive("tgDateSelector", ["$rootScope", "tgDatePickerConfigService", DateSelectorDirective]) ############################################################################# @@ -152,7 +123,7 @@ module.directive("tgSprintProgressbar", SprintProgressBarDirective) ## Created-by display directive ############################################################################# -CreatedByDisplayDirective = ($template, $compile, $translate)-> +CreatedByDisplayDirective = ($template, $compile, $translate, $navUrls)-> # Display the owner information (full name and photo) and the date of # creation of an object (like USs, tasks and issues). # @@ -168,13 +139,14 @@ CreatedByDisplayDirective = ($template, $compile, $translate)-> link = ($scope, $el, $attrs) -> render = (model) -> - owner = $scope.usersById?[model.owner] or { + owner = model.owner_extra_info or { full_name_display: $translate.instant("COMMON.EXTERNAL_USER") - photo: "/images/unnamed.png" + photo: "/images/user-noimage.png" } html = template({ owner: owner + url: if owner?.is_active then $navUrls.resolve("user-profile", {username: owner.username}) else "" date: moment(model.created_date).format($translate.instant("COMMON.DATETIME")) }) @@ -194,7 +166,8 @@ CreatedByDisplayDirective = ($template, $compile, $translate)-> require: "ngModel" } -module.directive("tgCreatedByDisplay", ["$tgTemplate", "$compile", "$translate", CreatedByDisplayDirective]) +module.directive("tgCreatedByDisplay", ["$tgTemplate", "$compile", "$translate", "$tgNavUrls", + CreatedByDisplayDirective]) ############################################################################# @@ -250,11 +223,7 @@ WatchersDirective = ($rootscope, $confirm, $repo, $qqueue, $template, $compile, html = $compile(template(ctx))($scope) $el.html(html) - if isEditable() and watchers.length == 0 - $el.find(".title").text("Add watchers") - $el.find(".watchers-header").addClass("no-watchers") - - $el.on "click", ".icon-delete", (event) -> + $el.on "click", ".js-delete-watcher", (event) -> event.preventDefault() return if not isEditable() target = angular.element(event.currentTarget) @@ -263,15 +232,15 @@ WatchersDirective = ($rootscope, $confirm, $repo, $qqueue, $template, $compile, title = $translate.instant("COMMON.WATCHERS.TITLE_LIGHTBOX_DELETE_WARTCHER") message = $scope.usersById[watcherId].full_name_display - $confirm.askOnDelete(title, message).then (finish) => - finish() + $confirm.askOnDelete(title, message).then (askResponse) => + askResponse.finish() watcherIds = _.clone($model.$modelValue.watchers, false) watcherIds = _.pull(watcherIds, watcherId) deleteWatcher(watcherIds) - $el.on "click", ".add-watcher", (event) -> + $el.on "click", ".js-add-watcher", (event) -> event.preventDefault() return if not isEditable() $scope.$apply -> @@ -353,8 +322,8 @@ AssignedToDirective = ($rootscope, $confirm, $repo, $loading, $qqueue, $template return if not isEditable() title = $translate.instant("COMMON.ASSIGNED_TO.CONFIRM_UNASSIGNED") - $confirm.ask(title).then (finish) => - finish() + $confirm.ask(title).then (response) => + response.finish() $model.$modelValue.assigned_to = null save(null) @@ -443,18 +412,18 @@ DeleteButtonDirective = ($log, $repo, $confirm, $location, $template) -> if not $attrs.onDeleteTitle return $log.error "DeleteButtonDirective requires on-delete-title set in scope." - $el.on "click", ".button", (event) -> + $el.on "click", ".button-delete", (event) -> title = $attrs.onDeleteTitle subtitle = $model.$modelValue.subject - $confirm.askOnDelete(title, subtitle).then (finish) => + $confirm.askOnDelete(title, subtitle).then (askResponse) => promise = $repo.remove($model.$modelValue) promise.then => - finish() + askResponse.finish() url = $scope.$eval($attrs.onDeleteGoToUrl) $location.path(url) promise.then null, => - finish(false) + askResponse.finish(false) $confirm.notify("error") $scope.$on "$destroy", -> @@ -554,7 +523,7 @@ module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$ ############################################################################# -## Editable subject directive +## Editable description directive ############################################################################# EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading, $selectedText, $qqueue, $template) -> @@ -603,6 +572,13 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading, $el.find('.view-description').hide() $el.find('textarea').focus() + $el.on "click", "a", (event) -> + target = angular.element(event.target) + href = target.attr('href') + if href.indexOf("#") == 0 + event.preventDefault() + $('body').scrollTop($(href).offset().top) + $el.on "click", ".save", (e) -> e.preventDefault() @@ -673,14 +649,14 @@ ListItemAssignedtoDirective = ($template) -> template = $template.get("common/components/list-item-assigned-to-avatar.html", true) link = ($scope, $el, $attrs) -> - bindOnce $scope, "membersById", (membersById) -> + bindOnce $scope, "usersById", (usersById) -> item = $scope.$eval($attrs.tgListitemAssignedto) ctx = {name: "Unassigned", imgurl: "/images/unnamed.png"} - member = membersById[item.assigned_to] + member = usersById[item.assigned_to] if member ctx.imgurl = member.photo - ctx.name = member.full_name + ctx.name = member.full_name_display $el.html(template(ctx)) diff --git a/app/coffee/modules/common/confirm.coffee b/app/coffee/modules/common/confirm.coffee index 28851121..e56b4347 100644 --- a/app/coffee/modules/common/confirm.coffee +++ b/app/coffee/modules/common/confirm.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -67,11 +67,12 @@ class ConfirmService extends taiga.Service currentLoading = @loading() .target(target) .start() - - defered.resolve (ok=true) => - currentLoading.finish() - if ok - @.hide(el) + defered.resolve { + finish: (ok=true) => + currentLoading.finish() + if ok + @.hide(el) + } el.on "click.confirm-dialog", "a.button-red", (event) => event.preventDefault() @@ -118,9 +119,10 @@ class ConfirmService extends taiga.Service .start() defered.resolve { selected: choicesField.val() - finish: => + finish: (ok=true) => currentLoading.finish() - @.hide(el) + if ok + @.hide(el) } el.on "click.confirm-dialog", "a.button-red", (event) => @@ -245,7 +247,7 @@ class ConfirmService extends taiga.Service delete @.tsem - el.on "click", ".icon-delete", (event) => + el.on "click", ".icon-delete, .close", (event) => body.find(selector) .removeClass('active') .addClass('inactive') diff --git a/app/coffee/modules/common/custom-field-values.coffee b/app/coffee/modules/common/custom-field-values.coffee index 8203d59f..65ec5438 100644 --- a/app/coffee/modules/common/custom-field-values.coffee +++ b/app/coffee/modules/common/custom-field-values.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -27,6 +27,28 @@ generateHash = taiga.generateHash module = angular.module("taigaCommon") +# Custom attributes types (see taiga-back/taiga/projects/custom_attributes/choices.py) +TEXT_TYPE = "text" +MULTILINE_TYPE = "multiline" +DATE_TYPE = "date" + + +TYPE_CHOICES = [ + { + key: TEXT_TYPE, + name: "ADMIN.CUSTOM_FIELDS.FIELD_TYPE_TEXT" + }, + { + key: MULTILINE_TYPE, + name: "ADMIN.CUSTOM_FIELDS.FIELD_TYPE_MULTI" + }, + { + key: DATE_TYPE, + name: "ADMIN.CUSTOM_FIELDS.FIELD_TYPE_DATE" + } +] + + class CustomAttributesValuesController extends taiga.Controller @.$inject = ["$scope", "$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$q"] @@ -118,85 +140,110 @@ CustomAttributesValuesDirective = ($templates, $storage) -> template: templateFn } -module.directive("tgCustomAttributesValues", ["$tgTemplate", "$tgStorage", CustomAttributesValuesDirective]) +module.directive("tgCustomAttributesValues", ["$tgTemplate", "$tgStorage", "$translate", + CustomAttributesValuesDirective]) -CustomAttributeValueDirective = ($template, $selectedText, $compile) -> +CustomAttributeValueDirective = ($template, $selectedText, $compile, $translate, datePickerConfigService) -> template = $template.get("custom-attributes/custom-attribute-value.html", true) templateEdit = $template.get("custom-attributes/custom-attribute-value-edit.html", true) link = ($scope, $el, $attrs, $ctrl) -> + prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT") + render = (attributeValue, edit=false) -> - value = attributeValue.value + if attributeValue.type is DATE_TYPE and attributeValue.value + value = moment(attributeValue.value, "YYYY-MM-DD").format(prettyDate) + else + value = attributeValue.value editable = isEditable() + ctx = { id: attributeValue.id name: attributeValue.name description: attributeValue.description value: value isEditable: editable + type: attributeValue.type } if editable and (edit or not value) html = templateEdit(ctx) html = $compile(html)($scope) + $el.html(html) + + if attributeValue.type == DATE_TYPE + datePickerConfig = datePickerConfigService.get() + _.merge(datePickerConfig, { + field: $el.find("input[name=value]")[0] + onSelect: (date) => + selectedDate = date + onOpen: => + $el.picker.setDate(selectedDate) if selectedDate? + }) + $el.picker = new Pikaday(datePickerConfig) else html = template(ctx) html = $compile(html)($scope) - - $el.html(html) + $el.html(html) isEditable = -> permissions = $scope.project.my_permissions requiredEditionPerm = $attrs.requiredEditionPerm return permissions.indexOf(requiredEditionPerm) > -1 - saveAttributeValue = -> - attributeValue.value = $el.find("input").val() + submit = debounce 2000, (event) => + event.preventDefault() + + attributeValue.value = $el.find("input[name=value], textarea[name='value']").val() + if attributeValue.type is DATE_TYPE + if moment(attributeValue.value, prettyDate).isValid() + attributeValue.value = moment(attributeValue.value, prettyDate).format("YYYY-MM-DD") + else + attributeValue.value = "" $scope.$apply -> $ctrl.updateAttributeValue(attributeValue).then -> render(attributeValue, false) - $el.on "keyup", "input[name=description]", (event) -> - if event.keyCode == 13 - submit(event) - else if event.keyCode == 27 - render(attributeValue, false) - - ## Actions (on view mode) - $el.on "click", ".custom-field-value.read-mode", -> - return if not isEditable() - return if $selectedText.get().length - render(attributeValue, true) - $el.find("input[name='description']").focus().select() - $scope.$apply() - - $el.on "click", "a.icon-edit", (event) -> - event.preventDefault() - render(attributeValue, true) - $el.find("input[name='description']").focus().select() - $scope.$apply() - - ## Actions (on edit mode) - submit = debounce 2000, (event) => - event.preventDefault() - saveAttributeValue() - - $el.on "submit", "form", submit - $el.on "click", "a.icon-floppy", submit - - $scope.$on "$destroy", -> - $el.off() + setFocusAndSelectOnInputField = -> + $el.find("input[name='value'], textarea[name='value']").focus().select() # Bootstrap attributeValue = $scope.$eval($attrs.tgCustomAttributeValue) render(attributeValue) + ## Actions (on view mode) + $el.on "click", ".js-value-view-mode", -> + return if not isEditable() + return if $selectedText.get().length + render(attributeValue, true) + setFocusAndSelectOnInputField() + + $el.on "click", "a.icon-edit", (event) -> + event.preventDefault() + render(attributeValue, true) + setFocusAndSelectOnInputField() + + ## Actions (on edit mode) + $el.on "keyup", "input[name=value], textarea[name='value']", (event) -> + if event.keyCode is 13 and event.currentTarget.type isnt "textarea" + submit(event) + else if event.keyCode == 27 + render(attributeValue, false) + + $el.on "submit", "form", submit + + $el.on "click", "a.icon-floppy", submit + + $scope.$on "$destroy", -> + $el.off() + return { link: link require: "^tgCustomAttributesValues" restrict: "AE" } -module.directive("tgCustomAttributeValue", ["$tgTemplate", "$selectedText", "$compile", CustomAttributeValueDirective]) +module.directive("tgCustomAttributeValue", ["$tgTemplate", "$selectedText", "$compile", "$translate", + "tgDatePickerConfigService", CustomAttributeValueDirective]) diff --git a/app/coffee/modules/common/estimation.coffee b/app/coffee/modules/common/estimation.coffee index b6ff0f5d..1b0fc1fd 100644 --- a/app/coffee/modules/common/estimation.coffee +++ b/app/coffee/modules/common/estimation.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -226,6 +226,10 @@ EstimationsService = ($template, $qqueue, $repo, $confirm, $q) -> @$el.find(".pop-points-open").show() + pop = @$el.find(".pop-points-open") + if pop.offset().top + pop.height() > document.body.clientHeight + pop.addClass('pop-bottom') + create = ($el, us, project) -> $el.unbind("click") diff --git a/app/coffee/modules/common/filters.coffee b/app/coffee/modules/common/filters.coffee index 27647dd6..71f4fae6 100644 --- a/app/coffee/modules/common/filters.coffee +++ b/app/coffee/modules/common/filters.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/common/history.coffee b/app/coffee/modules/common/history.coffee index 632d8086..aa548c88 100644 --- a/app/coffee/modules/common/history.coffee +++ b/app/coffee/modules/common/history.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -26,6 +26,14 @@ debounce = @.taiga.debounce module = angular.module("taigaCommon") +IGNORED_FIELDS = { + "userstories.userstory": [ + "watchers", "kanban_order", "backlog_order", "sprint_order", "finish_date" + ] + "tasks.task": [ + "watchers", "us_order", "taskboard_order" + ] +} ############################################################################# ## History Directive (Main) @@ -68,7 +76,7 @@ class HistoryController extends taiga.Controller return @rs.history.undeleteComment(type, objectId, activityId).then => @.loadHistory(type, objectId) -HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $compile) -> +HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $compile, $navUrls, $rootScope) -> templateChangeDiff = $template.get("common/history/history-change-diff.html", true) templateChangePoints = $template.get("common/history/history-change-points.html", true) templateChangeGeneric = $template.get("common/history/history-change-generic.html", true) @@ -136,15 +144,6 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c return humanizedFieldNames[field] or field - getUserFullName = (userId) -> - return $scope.usersById[userId]?.full_name_display - - getUserAvatar = (userId) -> - if $scope.usersById[userId]? - return $scope.usersById[userId].photo - else - return "/images/unnamed.png" - countChanges = (comment) -> return _.keys(comment.values_diff).length @@ -263,6 +262,10 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c return templateChangeGeneric({name:name, from:from, to: to}) renderChangeEntries = (change) -> + changeModel = change.key.split(":")[0] + if IGNORED_FIELDS[changeModel]? + change.values_diff = _.removeKeys(change.values_diff, IGNORED_FIELDS[changeModel]) + return _.map(change.values_diff, (value, field) -> renderChangeEntry(field, value)) renderChangesHelperText = (change) -> @@ -286,8 +289,9 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c return html[0].outerHTML html = templateActivity({ - avatar: getUserAvatar(comment.user.pk) + avatar: comment.user.photo userFullName: comment.user.name + userProfileUrl: if comment.user.is_active then $navUrls.resolve("user-profile", {username: comment.user.username}) else "" creationDate: moment(comment.created_at).format(getPrettyDateFormat()) comment: comment.comment_html changesText: renderChangesHelperText(comment) @@ -305,8 +309,9 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c renderChange = (change) -> return templateActivity({ - avatar: getUserAvatar(change.user.pk) + avatar: change.user.photo userFullName: change.user.name + userProfileUrl: if change.user.is_active then $navUrls.resolve("user-profile", {username: change.user.username}) else "" creationDate: moment(change.created_at).format(getPrettyDateFormat()) comment: change.comment_html changes: renderChangeEntries(change) @@ -359,6 +364,8 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c .start() onSuccess = -> + $rootScope.$broadcast("comment:new") + $ctrl.loadHistory(type, objectId).finally -> currentLoading.finish() @@ -385,6 +392,13 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c target = angular.element(event.currentTarget) save(target) + $el.on "click", "a", (event) -> + target = angular.element(event.target) + href = target.attr('href') + if href && href.indexOf("#") == 0 + event.preventDefault() + $('body').scrollTop($(href).offset().top) + $el.on "click", ".show-more", (event) -> event.preventDefault() @@ -419,8 +433,13 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c $(this).addClass('active') $el.on "click", ".history-tabs li a", (event) -> - $el.find(".history-tabs li a").toggleClass("active") - $el.find(".history section").toggleClass("hidden") + target = angular.element(event.currentTarget) + + $el.find(".history-tabs li a").removeClass("active") + target.addClass("active") + + $el.find(".history section").addClass("hidden") + $el.find(".history section.#{target.data('section-class')}").removeClass("hidden") $el.on "click", ".comment-delete", debounce 2000, (event) -> event.preventDefault() @@ -454,4 +473,4 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c module.directive("tgHistory", ["$log", "$tgLoading", "$tgQqueue", "$tgTemplate", "$tgConfirm", "$translate", - "$compile", HistoryDirective]) + "$compile", "$tgNavUrls", "$rootScope", HistoryDirective]) diff --git a/app/coffee/modules/common/importer.coffee b/app/coffee/modules/common/importer.coffee index 4027bf25..fbe18f4e 100644 --- a/app/coffee/modules/common/importer.coffee +++ b/app/coffee/modules/common/importer.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index 397e74b0..2bde4891 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -39,13 +39,14 @@ class LightboxService extends taiga.Service lightboxContent = $el.children().not(".close") lightboxContent.hide() - $el.css('display', 'flex') + @animationFrame.add -> + $el.css('display', 'flex') - @animationFrame.add => + @animationFrame.add -> $el.addClass("open") - @animationFrame.add -> - $el.find('input,textarea').first().focus() + @animationFrame.add -> + $el.find('input,textarea').first().focus() @animationFrame.add => lightboxContent.show() @@ -475,10 +476,9 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic } html = usersTemplate(ctx) - html = $compile(html)($scope) - $el.find("div.watchers").html(html) + $el.find(".assigned-to-list").html(html) closeLightbox = () -> lightboxKeyboardNavigationService.stop() @@ -499,7 +499,7 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic render(selectedUser, searchingText) $el.find('input').focus() - $el.on "click", ".watcher-single", (event) -> + $el.on "click", ".user-list-single", (event) -> event.preventDefault() target = angular.element(event.currentTarget) @@ -543,7 +543,7 @@ module.directive("tgLbAssignedto", ["lightboxService", "lightboxKeyboardNavigati ## Watchers Lightbox directive ############################################################################# -WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template) -> +WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template, $compile) -> link = ($scope, $el, $attrs) -> selectedItem = null usersTemplate = $template.get("common/lightbox/lightbox-assigned-to-users.html", true) @@ -572,7 +572,8 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS } html = usersTemplate(ctx) - $el.find("div.watchers").html(html) + html = $compile(html)($scope) + $el.find(".ticket-watchers").html(html) closeLightbox = () -> lightboxKeyboardNavigationService.stop() @@ -596,7 +597,7 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS render(users) $el.find("input").focus() - $el.on "click", ".watcher-single", debounce 2000, (event) -> + $el.on "click", ".user-list-single", debounce 2000, (event) -> closeLightbox() event.preventDefault() @@ -622,4 +623,37 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS link:link } -module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", WatchersLightboxDirective]) +module.directive("tgLbWatchers", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", WatchersLightboxDirective]) + + +############################################################################# +## Attachment Preview Lighbox +############################################################################# + +AttachmentPreviewLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template, $compile) -> + link = ($scope, $el, attrs) -> + template = $template.get("common/lightbox/lightbox-attachment-preview.html", true) + + $scope.$on "attachment:preview", (event, attachment) -> + lightboxService.open($el) + render(attachment) + + $scope.$on "$destroy", -> + $el.off() + + render = (attachment) -> + ctx = { + url: attachment.url, + title: attachment.description, + name: attachment.name + } + + html = template(ctx) + html = $compile(html)($scope) + $el.html(html) + + return { + link: link + } + +module.directive("tgLbAttachmentPreview", ["$tgRepo", "lightboxService", "lightboxKeyboardNavigationService", "$tgTemplate", "$compile", AttachmentPreviewLightboxDirective]) diff --git a/app/coffee/modules/common/loader.coffee b/app/coffee/modules/common/loader.coffee index 4ebc0215..f8f16707 100644 --- a/app/coffee/modules/common/loader.coffee +++ b/app/coffee/modules/common/loader.coffee @@ -1,9 +1,9 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino -# Copyright (C) 2014 Juan Francisco Alcántara -# Copyright (C) 2014 Alejandro Alonso +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino +# Copyright (C) 2014-2015 Juan Francisco Alcántara +# Copyright (C) 2014-2015 Alejandro Alonso # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -77,19 +77,11 @@ Loader = ($rootscope) -> lastResponseDate = 0 autoClose = () -> - maxAuto = 5000 - timeoutAuto = setTimeout (() -> - pageLoaded() - - clearInterval(intervalAuto) - ), maxAuto - intervalAuto = setInterval (() -> if lastResponseDate && requestCount == 0 pageLoaded() clearInterval(intervalAuto) - clearTimeout(timeoutAuto) ), 50 start = () -> diff --git a/app/coffee/modules/common/loading.coffee b/app/coffee/modules/common/loading.coffee index e2c0de6e..54e4d3fb 100644 --- a/app/coffee/modules/common/loading.coffee +++ b/app/coffee/modules/common/loading.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -21,19 +21,29 @@ module = angular.module("taigaCommon") -TgLoadingService = -> +TgLoadingService = ($compile) -> spinner = "loading..." return () -> service = { settings: { target: null, + scope: null, classes: [] - timeout: 0 + timeout: 0, + template: null }, target: (target) -> service.settings.target = target + return service + scope: (scope) -> + service.settings.scope = scope + + return service + template: (template) -> + service.settings.template = template + return service removeClasses: (classess...) -> service.settings.classes = classess @@ -51,9 +61,11 @@ TgLoadingService = -> # The loader is shown after that quantity of milliseconds timeoutId = setTimeout (-> if not target.hasClass('loading') - service.settings.oldContent = target.html() + if !service.settings.template + service.settings.template = target.html() target.addClass('loading') + target.html(spinner) ), service.settings.timeout @@ -71,27 +83,37 @@ TgLoadingService = -> removeClasses = service.settings.classes removeClasses.map (className) -> service.settings.target.addClass(className) - target.html(service.settings.oldContent) + target.html(service.settings.template) target.removeClass('loading') + if service.settings.scope + $compile(target.contents())(service.settings.scope) + return service } return service +TgLoadingService.$inject = [ + "$compile" +] + module.factory("$tgLoading", TgLoadingService) LoadingDirective = ($loading) -> link = ($scope, $el, attr) -> currentLoading = null + template = $el.html() $scope.$watch attr.tgLoading, (showLoading) => - if showLoading currentLoading = $loading() .target($el) + .timeout(50) + .template(template) + .scope($scope) .start() - else + else if currentLoading currentLoading.finish() return { diff --git a/app/coffee/modules/common/popovers.coffee b/app/coffee/modules/common/popovers.coffee index 329b661e..19d0abb6 100644 --- a/app/coffee/modules/common/popovers.coffee +++ b/app/coffee/modules/common/popovers.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/common/raven-logger.coffee b/app/coffee/modules/common/raven-logger.coffee index f0f9d415..217f2bf3 100644 --- a/app/coffee/modules/common/raven-logger.coffee +++ b/app/coffee/modules/common/raven-logger.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/common/tags.coffee b/app/coffee/modules/common/tags.coffee index ee655f32..f2856ce1 100644 --- a/app/coffee/modules/common/tags.coffee +++ b/app/coffee/modules/common/tags.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/common/wisiwyg.coffee b/app/coffee/modules/common/wisiwyg.coffee index 37ade9d3..d3b82c55 100644 --- a/app/coffee/modules/common/wisiwyg.coffee +++ b/app/coffee/modules/common/wisiwyg.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -149,6 +149,8 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans startIndex = result.index break + return if !result + regex = />>>/gi endIndex = 0 loop @@ -181,10 +183,19 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans onShiftEnter: {keepDefault:false, openWith:"\n\n"} onEnter: keepDefault: false, - replaceWith: () -> "\n" + replaceWith: () -> + # Allow textcomplete to intercept the enter key if the options list is displayed + # @todo There doesn't seem to be a more graceful way to do this with the textcomplete API. + if not $('.textcomplete-dropdown').is(':visible') + "\n" afterInsert: (data) -> lines = data.textarea.value.split("\n") - cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length + # Detect if we are in this situation +- aa at the beginning if the textarea + if data.caretPosition > 0 + cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length + else + cursorLine = 1 + newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0] lastLine = lines[cursorLine - 1] @@ -342,6 +353,104 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans element .markItUpRemove() .markItUp(markdownSettings) + .textcomplete([ + # us, task, and issue autocomplete: #id or # + { + cache: true + match: /(^|\s)#([a-z0-9]+)$/i, + search: (term, callback) -> + term = taiga.slugify(term) + + searchTypes = ['issues', 'tasks', 'userstories'] + searchProps = ['ref', 'subject'] + + filter = (item) => + for prop in searchProps + if taiga.slugify(item[prop]).indexOf(term) >= 0 + return true + return false + + $rs.search.do($scope.projectId, term).then (res) => + # ignore wikipages if they're the only results. can't exclude them in search + if res.count < 1 or res.count == res.wikipages.length + callback([]) + + else + for type in searchTypes + if res[type] and res[type].length > 0 + callback(res[type].filter(filter), true) + + # must signal end of lists + callback([]) + + replace: (res) -> + return "$1\##{res.ref} " + + template: (res, term) -> + return "\##{res.ref} - #{res.subject}" + } + + # username autocomplete: @username or @ + { + cache: true + match: /(^|\s)@([a-z0-9\-\._]{2,})$/i + search: (term, callback) -> + username = taiga.slugify(term) + searchProps = ['username', 'full_name', 'full_name_display'] + + if $scope.project.members.length < 1 + callback([]) + + else + callback $scope.project.members.filter (user) => + for prop in searchProps + if taiga.slugify(user[prop]).indexOf(username) >= 0 + return true + return false + + replace: (user) -> + return "$1@#{user.username} " + + template: (user) -> + return "#{user.username} - #{user.full_name_display}" + } + + # wiki pages autocomplete: [[slug or [[ + # if the search function was called with the 3rd param the regex + # like the docs claim, we could combine this with the #123 search + { + cache: true + match: /(^|\s)\[\[([a-z0-9\-]+)$/i + search: (term, callback) -> + term = taiga.slugify(term) + + $rs.search.do($scope.projectId, term).then (res) => + if res.count < 1 + callback([]) + + if res.count < 1 or not res.wikipages or res.wikipages.length <= 0 + callback([]) + + else + callback res.wikipages.filter((page) => + return taiga.slugify(page['slug']).indexOf(term) >= 0 + ), true + + # must signal end of lists + callback([]) + + + replace: (res) -> + return "$1[[#{res.slug}]]" + + template: (res, term) -> + return res.slug + } + ], + { + debounce: 200 + } + ) renderMarkItUp() diff --git a/app/coffee/modules/controllerMixins.coffee b/app/coffee/modules/controllerMixins.coffee index 34b5adb6..708e8e7f 100644 --- a/app/coffee/modules/controllerMixins.coffee +++ b/app/coffee/modules/controllerMixins.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -41,9 +41,9 @@ class PageMixin @scope.usersById = groupBy(@scope.users, (e) -> e.id) @scope.roles = _.sortBy(roles, "order") - availableRoles = _(@scope.project.memberships).map("role").uniq().value() + computableRoles = _(@scope.project.members).map("role").uniq().value() @scope.computableRoles = _(roles).filter("computable") - .filter((x) -> _.contains(availableRoles, x.id)) + .filter((x) -> _.contains(computableRoles, x.id)) .value() loadUsersAndRoles: -> promise = @q.all([ diff --git a/app/coffee/modules/events.coffee b/app/coffee/modules/events.coffee index bd8923c1..64ac5a80 100644 --- a/app/coffee/modules/events.coffee +++ b/app/coffee/modules/events.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/feedback.coffee b/app/coffee/modules/feedback.coffee index d121c889..d645cb9d 100644 --- a/app/coffee/modules/feedback.coffee +++ b/app/coffee/modules/feedback.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/integrations.coffee b/app/coffee/modules/integrations.coffee index 368e313c..5390e236 100644 --- a/app/coffee/modules/integrations.coffee +++ b/app/coffee/modules/integrations.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/issues.coffee b/app/coffee/modules/issues.coffee index c95f3992..82d8031f 100644 --- a/app/coffee/modules/issues.coffee +++ b/app/coffee/modules/issues.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index 729584a1..dffcb92b 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -26,6 +26,7 @@ toString = @.taiga.toString joinStr = @.taiga.joinStr groupBy = @.taiga.groupBy bindOnce = @.taiga.bindOnce +bindMethods = @.taiga.bindMethods module = angular.module("taigaIssues") @@ -52,6 +53,8 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @log, @appMetaService, @analytics, @navUrls, @translate) -> + bindMethods(@) + @scope.issueRef = @params.issueref @scope.sectionName = @translate.instant("ISSUES.SECTION_NAME") @.initializeEventHandlers() @@ -83,20 +86,16 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) initializeEventHandlers: -> @scope.$on "attachment:create", => - @rootscope.$broadcast("object:updated") @analytics.trackEvent("attachment", "create", "create attachment on issue", 1) - @scope.$on "attachment:edit", => - @rootscope.$broadcast("object:updated") - - @scope.$on "attachment:delete", => - @rootscope.$broadcast("object:updated") - @scope.$on "promote-issue-to-us:success", => @analytics.trackEvent("issue", "promoteToUserstory", "promote issue to userstory", 1) @rootscope.$broadcast("object:updated") @.loadIssue() + @scope.$on "comment:new", => + @.loadIssue() + @scope.$on "custom-attributes-values:edit", => @rootscope.$broadcast("object:updated") @@ -120,7 +119,6 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.severityById = groupBy(project.severities, (x) -> x.id) @scope.priorityList = project.priorities @scope.priorityById = groupBy(project.priorities, (x) -> x.id) - @scope.membersById = groupBy(project.memberships, (x) -> x.user) return project loadIssue: -> @@ -146,9 +144,52 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) loadInitialData: -> promise = @.loadProject() return promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @.loadIssue() + ### + # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button. + # See app/modules/components/vote-button for more info + ### + onUpvote: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.upvote(@scope.issueId).then(onSuccess, onError) + + onDownvote: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.downvote(@scope.issueId).then(onSuccess, onError) + + ### + # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button. + # See app/modules/components/watch-button for more info + ### + onWatch: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.watch(@scope.issueId).then(onSuccess, onError) + + onUnwatch: -> + onSuccess = => + @.loadIssue() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.issues.unwatch(@scope.issueId).then(onSuccess, onError) module.controller("IssueDetailController", IssueDetailController) @@ -558,7 +599,7 @@ module.directive("tgIssuePriorityButton", ["$rootScope", "$tgRepo", "$tgConfirm" PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm, $qqueue, $translate) -> link = ($scope, $el, $attrs, $model) -> - save = $qqueue.bindAdd (issue, finish) => + save = $qqueue.bindAdd (issue, askResponse) => data = { generated_from_issue: issue.id project: issue.project, @@ -570,12 +611,12 @@ PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm, $qqueue, $transl } onSuccess = -> - finish() + askResponse.finish() $confirm.notify("success") $rootScope.$broadcast("promote-issue-to-us:success") onError = -> - finish(false) + askResponse.finish() $confirm.notify("error") $repo.create("userstories", data).then(onSuccess, onError) @@ -589,9 +630,8 @@ PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm, $qqueue, $transl message = $translate.instant("ISSUES.CONFIRM_PROMOTE.MESSAGE") subtitle = issue.subject - $confirm.ask(title, subtitle, message).then (finish) => - save(issue, finish) - + $confirm.ask(title, subtitle, message).then (response) => + save(issue, response) $scope.$on "$destroy", -> $el.off() diff --git a/app/coffee/modules/issues/lightboxes.coffee b/app/coffee/modules/issues/lightboxes.coffee index 29052e8f..c8db0cdf 100644 --- a/app/coffee/modules/issues/lightboxes.coffee +++ b/app/coffee/modules/issues/lightboxes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/issues/list.coffee b/app/coffee/modules/issues/list.coffee index 1e3908c3..1b90695e 100644 --- a/app/coffee/modules/issues/list.coffee +++ b/app/coffee/modules/issues/list.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -83,8 +83,6 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.$on "issueform:new:success", => @analytics.trackEvent("issue", "create", "create issue on issues list", 1) @.loadIssues() - @.loadFilters() - initializeSubscription: -> routingKey = "changes.project.#{@scope.projectId}.issues" @@ -112,13 +110,13 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.issueTypes = _.sortBy(project.issue_types, "order") @scope.issueTypeById = groupBy(project.issue_types, (x) -> x.id) - @scope.membersById = groupBy(project.memberships, (x) -> x.user) return project getUrlFilters: -> - filters = _.pick(@location.search(), "page", "tags", "statuses", "types", + filters = _.pick(@location.search(), "page", "tags", "status", "types", "q", "severities", "priorities", "assignedTo", "createdBy", "orderBy") + filters.page = 1 if not filters.page return filters @@ -181,20 +179,30 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.filters.myFilters = myFilters return myFilters + loadFilters = {} + loadFilters.project = @scope.projectId + loadFilters.tags = urlfilters.tags + loadFilters.status = urlfilters.status + loadFilters.q = urlfilters.q + loadFilters.types = urlfilters.types + loadFilters.severities = urlfilters.severities + loadFilters.priorities = urlfilters.priorities + loadFilters.assigned_to = urlfilters.assignedTo + loadFilters.owner = urlfilters.createdBy + # Load default filters data promise = promise.then => - return @rs.issues.filtersData(@scope.projectId) + return @rs.issues.filtersData(loadFilters) # Format filters and set them on scope return promise.then (data) => usersFiltersFormat = (users, type, unknownOption) => reformatedUsers = _.map users, (t) => - return { - id: t[0], - count: t[1], - type: type - name: if t[0] then @scope.usersById[t[0]].full_name_display else unknownOption - } + t.type = type + t.name = if t.full_name then t.full_name else unknownOption + + return t + unknownItem = _.remove(reformatedUsers, (u) -> not u.id) reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase()) if unknownItem.length > 0 @@ -203,34 +211,27 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi choicesFiltersFormat = (choices, type, byIdObject) => _.map choices, (t) -> - return { - id: t[0], - name: byIdObject[t[0]].name, - color: byIdObject[t[0]].color, - count: t[1], - type: type} + t.type = type + return t tagsFilterFormat = (tags) => - return _.map tags, (t) => - return { - id: t[0], - name: t[0], - color: @scope.project.tags_colors[t[0]], - count: t[1], - type: "tags" - } + return _.map tags, (t) -> + t.id = t.name + t.type = 'tags' + return t # Build filters data structure - @scope.filters.statuses = choicesFiltersFormat(data.statuses, "statuses", @scope.issueStatusById) + @scope.filters.status = choicesFiltersFormat(data.statuses, "status", @scope.issueStatusById) @scope.filters.severities = choicesFiltersFormat(data.severities, "severities", @scope.severityById) @scope.filters.priorities = choicesFiltersFormat(data.priorities, "priorities", @scope.priorityById) @scope.filters.assignedTo = usersFiltersFormat(data.assigned_to, "assignedTo", "Unassigned") - @scope.filters.createdBy = usersFiltersFormat(data.created_by, "createdBy", "Unknown") + @scope.filters.createdBy = usersFiltersFormat(data.owners, "createdBy", "Unknown") @scope.filters.types = choicesFiltersFormat(data.types, "types", @scope.issueTypeById) @scope.filters.tags = tagsFilterFormat(data.tags) @.removeNotExistingFiltersFromUrl() @.markSelectedFilters(@scope.filters, urlfilters) + @rootscope.$broadcast("filters:loaded", @scope.filters) # We need to guarantee that the last petition done here is the finally used @@ -258,7 +259,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi name = "assigned_to" else if name == "createdBy" name = "owner" - else if name == "statuses" + else if name == "status" name = "status" else if name == "types" name = "type" @@ -273,14 +274,19 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.page = data.current @scope.count = data.count @scope.paginatedBy = data.paginatedBy + return data + return promise + loadInitialData: -> promise = @.loadProject() return promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @.initializeSubscription() - return @q.all([@.loadFilters(), @.loadIssues()]) + @.loadFilters() + + return @.loadIssues() saveCurrentFiltersTo: (newFilter) -> deferred = @q.defer() @@ -401,7 +407,7 @@ IssuesDirective = ($log, $location, $template, $compile) -> # Draw the arrow the first time currentOrder = $ctrl.getUrlFilter("orderBy") or "created_date" if currentOrder - icon = if startswith(currentOrder, "-") then "icon-caret-up" else "icon-caret-down" + icon = if startswith(currentOrder, "-") then "icon-arrow-up" else "icon-arrow-bottom" colHeadElement = $el.find(".row.title > div[data-fieldname='#{trim(currentOrder, "-")}']") colHeadElement.html("#{colHeadElement.html()}") @@ -419,7 +425,7 @@ IssuesDirective = ($log, $location, $template, $compile) -> $ctrl.loadIssues().then -> # Update the arrow $el.find(".row.title > div > span.icon").remove() - icon = if startswith(finalOrder, "-") then "icon-caret-up" else "icon-caret-down" + icon = if startswith(finalOrder, "-") then "icon-arrow-up" else "icon-arrow-bottom" target.html("#{target.html()}") ## Issues Link @@ -440,12 +446,13 @@ module.directive("tgIssues", ["$log", "$tgLocation", "$tgTemplate", "$compile", ## Issues Filters Directive ############################################################################# -IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $translate, $compile, $auth) -> +IssuesFiltersDirective = ($q, $log, $location, $rs, $confirm, $loading, $template, $translate, $compile, $auth) -> template = $template.get("issue/issues-filters.html", true) templateSelected = $template.get("issue/issues-filters-selected.html", true) link = ($scope, $el, $attrs) -> $ctrl = $el.closest(".wrapper").controller() + selectedFilters = [] showFilters = (title, type) -> @@ -491,6 +498,16 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ html = $compile(html)($scope) $el.find(".filter-list").html(html) + getFiltersType = () -> + return $el.find("h2 a.subfilter span.title").prop('data-type') + + reloadIssues = () -> + currentFiltersType = getFiltersType() + + $q.all([$ctrl.loadIssues(), $ctrl.loadFilters()]).then () -> + filters = $scope.filters[currentFiltersType] + renderFilters(_.reject(filters, "selected")) + toggleFilterSelection = (type, id) -> if type == "myFilters" $rs.issues.getMyFilters($scope.projectId).then (data) -> @@ -507,7 +524,6 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ filters = $scope.filters[type] filterId = if type == 'tags' then taiga.toString(id) else id filter = _.find(filters, {id: filterId}) - filter.selected = (not filter.selected) # Convert id to null as string for properly @@ -516,22 +532,23 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ if filter.selected selectedFilters.push(filter) - $scope.$apply -> - $ctrl.selectFilter(type, id) - $ctrl.selectFilter("page", 1) - $ctrl.storeFilters() - $ctrl.loadIssues() + $ctrl.selectFilter(type, id) + $ctrl.selectFilter("page", 1) + $ctrl.storeFilters() else - selectedFilters = _.reject(selectedFilters, filter) - $scope.$apply -> - $ctrl.unselectFilter(type, id) - $ctrl.selectFilter("page", 1) - $ctrl.storeFilters() - $ctrl.loadIssues() + selectedFilters = _.reject selectedFilters, (f) -> + return f.id == filter.id && f.type == filter.type + + $ctrl.unselectFilter(type, id) + $ctrl.selectFilter("page", 1) + $ctrl.storeFilters() + + reloadIssues() renderSelectedFilters(selectedFilters) - currentFiltersType = $el.find("h2 a.subfilter span.title").prop('data-type') + currentFiltersType = getFiltersType() + if type == currentFiltersType renderFilters(_.reject(filters, "selected")) @@ -540,7 +557,7 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ initializeSelectedFilters(filters) $scope.$on "filters:issueupdate", (ctx, filters) -> - html = template({filters:filters.statuses}) + html = template({filters:filters.status}) html = $compile(html)($scope) $el.find(".filter-list").html(html) @@ -555,7 +572,8 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ else $ctrl.replaceFilter("q", value) $ctrl.storeFilters() - $ctrl.loadIssues() + + reloadIssues() $scope.$watch("filtersQ", selectQFilter) @@ -602,18 +620,18 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ title = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.TITLE") message = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.MESSAGE", {customFilterName: customFilterName}) - $confirm.askOnDelete(title, message).then (finish) -> + $confirm.askOnDelete(title, message).then (askResponse) -> promise = $ctrl.deleteMyFilter(customFilterName) promise.then -> promise = $ctrl.loadMyFilters() promise.then (filters) -> - finish() + askResponse.finish() $scope.filters.myFilters = filters renderFilters($scope.filters.myFilters) promise.then null, -> - finish() + askResponse.finish() promise.then null, -> - finish(false) + askResponse.finish(false) $confirm.notify("error") @@ -664,7 +682,7 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ return {link:link} -module.directive("tgIssuesFilters", ["$log", "$tgLocation", "$tgResources", "$tgConfirm", "$tgLoading", +module.directive("tgIssuesFilters", ["$q", "$log", "$tgLocation", "$tgResources", "$tgConfirm", "$tgLoading", "$tgTemplate", "$translate", "$compile", "$tgAuth", IssuesFiltersDirective]) @@ -711,7 +729,7 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) -> event.stopPropagation() target = angular.element(event.currentTarget) - for filter in $scope.filters.statuses + for filter in $scope.filters.status if filter.id == issue.status filter.count-- @@ -723,15 +741,10 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) -> $repo.save(issue).then -> $ctrl.loadIssues() - for filter in $scope.filters.statuses - if filter.id == issue.status - filter.count++ - - $rootscope.$broadcast("filters:issueupdate", $scope.filters) - - for filter in $scope.filters.statuses + for filter in $scope.filters.status if filter.id == issue.status filter.count++ + $rootscope.$broadcast("filters:issueupdate", $scope.filters) taiga.bindOnce $scope, "project", (project) -> diff --git a/app/coffee/modules/kanban.coffee b/app/coffee/modules/kanban.coffee index 0e6a22ad..f4986966 100644 --- a/app/coffee/modules/kanban.coffee +++ b/app/coffee/modules/kanban.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee index b49610e3..b2f803ba 100644 --- a/app/coffee/modules/kanban/main.coffee +++ b/app/coffee/modules/kanban/main.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -34,14 +34,10 @@ module = angular.module("taigaKanban") # Vars defaultViewMode = "maximized" -defaultViewModes = { - maximized: { - cardClass: "kanban-task-maximized" - } - minimized: { - cardClass: "kanban-task-minimized" - } -} +viewModes = [ + "maximized", + "minimized" +] ############################################################################# @@ -157,6 +153,10 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi usByStatus[status.id] = _.sortBy(usByStatus[status.id], "kanban_order") + if userstories.length == 0 + status = @scope.usStatusList[0] + usByStatus[status.id].push({isPlaceholder: true}) + @scope.usByStatus = usByStatus # The broadcast must be executed when the DOM has been fully reloaded. @@ -209,7 +209,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi loadInitialData: -> promise = @.loadProject() return promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @.initializeSubscription() @.loadKanban().then( => @scope.$broadcast("redraw:wip")) @@ -221,8 +221,9 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.statusViewModes = {} for status in @scope.usStatusList - mode = storedStatusViewModes[status.id] - @scope.statusViewModes[status.id] = if _.has(defaultViewModes, mode) then mode else defaultViewMode + mode = storedStatusViewModes[status.id] || defaultViewMode + + @scope.statusViewModes[status.id] = mode @.storeStatusViewModes() @@ -233,9 +234,13 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.statusViewModes[statusId] = newViewMode @.storeStatusViewModes() - getCardClass: (statusId)-> + isMaximized: (statusId) -> mode = @scope.statusViewModes[statusId] or defaultViewMode - return defaultViewModes[mode].cardClass or defaultViewModes[defaultViewMode].cardClass + return mode == 'maximized' + + isMinimized: (statusId) -> + mode = @scope.statusViewModes[statusId] or defaultViewMode + return mode == 'minimized' # Utils methods @@ -317,7 +322,7 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) -> status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader) hidden = true - $scope.class = "icon icon-open-eye" + $scope.class = "icon-open-eye" $scope.title = showArchivedText $el.on "click", (event) -> @@ -325,12 +330,12 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) -> $scope.$apply -> if hidden - $scope.class = "icon icon-open-eye" + $scope.class = "icon-open-eye" $scope.title = showArchivedText $rootscope.$broadcast("kanban:hide-userstories-for-status", status.id) else - $scope.class = "icon icon-closed-eye" + $scope.class = "icon-closed-eye" $scope.title = hideArchivedText $rootscope.$broadcast("kanban:show-userstories-for-status", status.id) @@ -414,7 +419,7 @@ KanbanUserstoryDirective = ($rootscope, $loading, $rs) -> else if not us.is_blocked and $el.hasClass("blocked") $el.removeClass("blocked") - $el.find(".icon-edit").on "click", (event) -> + $el.on 'click', '.icon-edit', (event) -> if $el.find(".icon-edit").hasClass("noclick") return @@ -431,11 +436,17 @@ KanbanUserstoryDirective = ($rootscope, $loading, $rs) -> $rootscope.$broadcast("usform:edit", editingUserStory) currentLoading.finish() + $scope.getTemplateUrl = () -> + if $scope.us.isPlaceholder + return "common/components/kanban-placeholder.html" + else + return "kanban/kanban-task.html" + $scope.$on "$destroy", -> $el.off() return { - templateUrl: "kanban/kanban-task.html" + template: '', link: link require: "ngModel" } diff --git a/app/coffee/modules/kanban/sortable.coffee b/app/coffee/modules/kanban/sortable.coffee index ac86e649..58d5f678 100644 --- a/app/coffee/modules/kanban/sortable.coffee +++ b/app/coffee/modules/kanban/sortable.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/projects.coffee b/app/coffee/modules/projects.coffee index c4b08aec..7fccd406 100644 --- a/app/coffee/modules/projects.coffee +++ b/app/coffee/modules/projects.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/projects/lightboxes.coffee b/app/coffee/modules/projects/lightboxes.coffee index 6bdf068f..fe1b42ce 100644 --- a/app/coffee/modules/projects/lightboxes.coffee +++ b/app/coffee/modules/projects/lightboxes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -47,7 +47,7 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project $location.url($projectUrl.get(response)) lightboxService.close($el) - currentUserService._loadProjects() + currentUserService.loadProjects() onErrorSubmit = (response) -> currentLoading.finish() @@ -74,10 +74,7 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project promise.then(onSuccessSubmit, onErrorSubmit) openLightbox = -> - $scope.data = { - total_story_points: 100 - total_milestones: 5 - } + $scope.data = {} if !$scope.templates.length $rs.projects.templates().then (result) => @@ -173,7 +170,7 @@ DeleteProjectDirective = ($repo, $rootscope, $auth, $location, $navUrls, $confir $rootscope.$broadcast("projects:reload") $location.path($navUrls.resolve("home")) $confirm.notify("success") - currentUserService._loadProjects() + currentUserService.loadProjects() # FIXME: error handling? promise.then null, -> diff --git a/app/coffee/modules/related-tasks.coffee b/app/coffee/modules/related-tasks.coffee index f2ef3bfa..ec0cbd8b 100644 --- a/app/coffee/modules/related-tasks.coffee +++ b/app/coffee/modules/related-tasks.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -85,14 +85,15 @@ RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading, $tem task = $model.$modelValue message = task.subject - $confirm.askOnDelete(title, message).then (finish) -> + $confirm.askOnDelete(title, message).then (askResponse) -> promise = $repo.remove(task) promise.then -> - finish() + askResponse.finish() $confirm.notify("success") $scope.$emit("related-tasks:delete") promise.then null, -> + askResponse.finish(false) $confirm.notify("error") $scope.$watch $attrs.ngModel, (val) -> @@ -216,7 +217,7 @@ RelatedTasksDirective = ($repo, $rs, $rootscope) -> link = ($scope, $el, $attrs) -> loadTasks = -> return $rs.tasks.list($scope.projectId, null, $scope.usId).then (tasks) => - $scope.tasks = tasks + $scope.tasks = _.sortBy(tasks, 'ref') return tasks $scope.$on "related-tasks:add", -> diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index 991133c9..f5453640 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -37,8 +37,11 @@ urls = { "users-change-password": "/users/change_password" "users-change-email": "/users/change_email" "users-cancel-account": "/users/cancel" - "contacts": "/users/%s/contacts" - "stats": "/users/%s/stats" + "user-stats": "/users/%s/stats" + "user-liked": "/users/%s/liked" + "user-voted": "/users/%s/voted" + "user-watched": "/users/%s/watched" + "user-contacts": "/users/%s/contacts" # User - Notification "permissions": "/permissions" @@ -63,6 +66,10 @@ urls = { "project-templates": "/project-templates" "project-modules": "/projects/%s/modules" "bulk-update-projects-order": "/projects/bulk_update_order" + "project-like": "/projects/%s/like" + "project-unlike": "/projects/%s/unlike" + "project-watch": "/projects/%s/watch" + "project-unwatch": "/projects/%s/unwatch" # Project Values - Choises "userstory-statuses": "/userstory-statuses" @@ -82,15 +89,29 @@ urls = { "bulk-update-us-backlog-order": "/userstories/bulk_update_backlog_order" "bulk-update-us-sprint-order": "/userstories/bulk_update_sprint_order" "bulk-update-us-kanban-order": "/userstories/bulk_update_kanban_order" + "userstories-filters": "/userstories/filters_data" + "userstory-upvote": "/userstories/%s/upvote" + "userstory-downvote": "/userstories/%s/downvote" + "userstory-watch": "/userstories/%s/watch" + "userstory-unwatch": "/userstories/%s/unwatch" # Tasks "tasks": "/tasks" "bulk-create-tasks": "/tasks/bulk_create" "bulk-update-task-taskboard-order": "/tasks/bulk_update_taskboard_order" + "task-upvote": "/tasks/%s/upvote" + "task-downvote": "/tasks/%s/downvote" + "task-watch": "/tasks/%s/watch" + "task-unwatch": "/tasks/%s/unwatch" # Issues "issues": "/issues" "bulk-create-issues": "/issues/bulk_create" + "issues-filters": "/issues/filters_data" + "issue-upvote": "/issues/%s/upvote" + "issue-downvote": "/issues/%s/downvote" + "issue-watch": "/issues/%s/watch" + "issue-unwatch": "/issues/%s/unwatch" # Wiki pages "wiki": "/wiki" @@ -147,6 +168,10 @@ urls = { # locales "locales": "/locales" + + # Application tokens + "applications": "/applications" + "application-tokens": "/application-tokens" } # Initialize api urls service diff --git a/app/coffee/modules/resources/attachments.coffee b/app/coffee/modules/resources/attachments.coffee index ef30ebd5..910145c2 100644 --- a/app/coffee/modules/resources/attachments.coffee +++ b/app/coffee/modules/resources/attachments.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/custom-attributes-values.coffee b/app/coffee/modules/resources/custom-attributes-values.coffee index 5322c639..1b2c4fd7 100644 --- a/app/coffee/modules/resources/custom-attributes-values.coffee +++ b/app/coffee/modules/resources/custom-attributes-values.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/custom-attributes.coffee b/app/coffee/modules/resources/custom-attributes.coffee index cf14c398..d5734e26 100644 --- a/app/coffee/modules/resources/custom-attributes.coffee +++ b/app/coffee/modules/resources/custom-attributes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/history.coffee b/app/coffee/modules/resources/history.coffee index 3e61a688..e40ee9f8 100644 --- a/app/coffee/modules/resources/history.coffee +++ b/app/coffee/modules/resources/history.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/invitations.coffee b/app/coffee/modules/resources/invitations.coffee index 5cdbcecc..b4a5d2c5 100644 --- a/app/coffee/modules/resources/invitations.coffee +++ b/app/coffee/modules/resources/invitations.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/issues.coffee b/app/coffee/modules/resources/issues.coffee index bf7c27df..521c8685 100644 --- a/app/coffee/modules/resources/issues.coffee +++ b/app/coffee/modules/resources/issues.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -55,11 +55,27 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) -> params = {project_id: projectId, bulk_issues: data} return $http.post(url, params) + service.upvote = (issueId) -> + url = $urls.resolve("issue-upvote", issueId) + return $http.post(url) + + service.downvote = (issueId) -> + url = $urls.resolve("issue-downvote", issueId) + return $http.post(url) + + service.watch = (issueId) -> + url = $urls.resolve("issue-watch", issueId) + return $http.post(url) + + service.unwatch = (issueId) -> + url = $urls.resolve("issue-unwatch", issueId) + return $http.post(url) + service.stats = (projectId) -> return $repo.queryOneRaw("projects", "#{projectId}/issues_stats") - service.filtersData = (projectId) -> - return $repo.queryOneRaw("projects", "#{projectId}/issue_filters_data") + service.filtersData = (params) -> + return $repo.queryOneRaw("issues-filters", null, params) service.listValues = (projectId, type) -> params = {"project": projectId} diff --git a/app/coffee/modules/resources/kanban.coffee b/app/coffee/modules/resources/kanban.coffee index b1268dfe..e71c4cee 100644 --- a/app/coffee/modules/resources/kanban.coffee +++ b/app/coffee/modules/resources/kanban.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/mdrender.coffee b/app/coffee/modules/resources/mdrender.coffee index b3c8b086..8a8d58ae 100644 --- a/app/coffee/modules/resources/mdrender.coffee +++ b/app/coffee/modules/resources/mdrender.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/memberships.coffee b/app/coffee/modules/resources/memberships.coffee index 09443fd7..50c0f0cf 100644 --- a/app/coffee/modules/resources/memberships.coffee +++ b/app/coffee/modules/resources/memberships.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/modules.coffee b/app/coffee/modules/resources/modules.coffee index fcb6e6a7..b0e7ef45 100644 --- a/app/coffee/modules/resources/modules.coffee +++ b/app/coffee/modules/resources/modules.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules.coffee +### + resourceProvider = ($repo) -> service = {} diff --git a/app/coffee/modules/resources/notify-policies.coffee b/app/coffee/modules/resources/notify-policies.coffee index 654bc5db..60b4b6aa 100644 --- a/app/coffee/modules/resources/notify-policies.coffee +++ b/app/coffee/modules/resources/notify-policies.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/projects.coffee b/app/coffee/modules/resources/projects.coffee index b7243a33..9747d2e1 100644 --- a/app/coffee/modules/resources/projects.coffee +++ b/app/coffee/modules/resources/projects.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/roles.coffee b/app/coffee/modules/resources/roles.coffee index 9bcbf950..591f8681 100644 --- a/app/coffee/modules/resources/roles.coffee +++ b/app/coffee/modules/resources/roles.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/search.coffee b/app/coffee/modules/resources/search.coffee index 505b3e7f..a452b4ae 100644 --- a/app/coffee/modules/resources/search.coffee +++ b/app/coffee/modules/resources/search.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/sprints.coffee b/app/coffee/modules/resources/sprints.coffee index d9f5ccdc..e8535b5b 100644 --- a/app/coffee/modules/resources/sprints.coffee +++ b/app/coffee/modules/resources/sprints.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -39,12 +39,20 @@ resourceProvider = ($repo, $model, $storage) -> service.list = (projectId, filters) -> params = {"project": projectId} params = _.extend({}, params, filters or {}) - return $repo.queryMany("milestones", params).then (milestones) => + return $repo.queryMany("milestones", params, {}, true).then (result) => + milestones = result[0] + headers = result[1] + for m in milestones uses = m.user_stories uses = _.map(uses, (u) => $model.make_model("userstories", u)) m._attrs.user_stories = uses - return milestones + + return { + milestones: milestones, + closed: parseInt(headers("Taiga-Info-Total-Closed-Milestones"), 10), + open: parseInt(headers("Taiga-Info-Total-Opened-Milestones"), 10) + } return (instance) -> diff --git a/app/coffee/modules/resources/tasks.coffee b/app/coffee/modules/resources/tasks.coffee index 7069012c..5eaa1abf 100644 --- a/app/coffee/modules/resources/tasks.coffee +++ b/app/coffee/modules/resources/tasks.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -57,6 +57,22 @@ resourceProvider = ($repo, $http, $urls, $storage) -> return $http.post(url, params).then (result) -> return result.data + service.upvote = (taskId) -> + url = $urls.resolve("task-upvote", taskId) + return $http.post(url) + + service.downvote = (taskId) -> + url = $urls.resolve("task-downvote", taskId) + return $http.post(url) + + service.watch = (taskId) -> + url = $urls.resolve("task-watch", taskId) + return $http.post(url) + + service.unwatch = (taskId) -> + url = $urls.resolve("task-unwatch", taskId) + return $http.post(url) + service.bulkUpdateTaskTaskboardOrder = (projectId, data) -> url = $urls.resolve("bulk-update-task-taskboard-order") params = {project_id: projectId, bulk_tasks: data} diff --git a/app/coffee/modules/resources/user-settings.coffee b/app/coffee/modules/resources/user-settings.coffee index 54a374cc..ca3d365e 100644 --- a/app/coffee/modules/resources/user-settings.coffee +++ b/app/coffee/modules/resources/user-settings.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/resources/users.coffee b/app/coffee/modules/resources/users.coffee index 3db62b96..53285191 100644 --- a/app/coffee/modules/resources/users.coffee +++ b/app/coffee/modules/resources/users.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -28,7 +28,7 @@ resourceProvider = ($http, $urls) -> service = {} service.contacts = (userId, options={}) -> - url = $urls.resolve("contacts", userId) + url = $urls.resolve("user-contacts", userId) httpOptions = {headers: {}} if not options.enablePagination diff --git a/app/coffee/modules/resources/userstories.coffee b/app/coffee/modules/resources/userstories.coffee index de2f8c9b..0ac89b38 100644 --- a/app/coffee/modules/resources/userstories.coffee +++ b/app/coffee/modules/resources/userstories.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -41,6 +41,9 @@ resourceProvider = ($repo, $http, $urls, $storage) -> service.listInAllProjects = (filters) -> return $repo.queryMany("userstories", filters) + service.filtersData = (params) -> + return $repo.queryOneRaw("userstories-filters", null, params) + service.listUnassigned = (projectId, filters) -> params = {"project": projectId, "milestone": "null"} params = _.extend({}, params, filters or {}) @@ -64,6 +67,22 @@ resourceProvider = ($repo, $http, $urls, $storage) -> return $http.post(url, data) + service.upvote = (userStoryId) -> + url = $urls.resolve("userstory-upvote", userStoryId) + return $http.post(url) + + service.downvote = (userStoryId) -> + url = $urls.resolve("userstory-downvote", userStoryId) + return $http.post(url) + + service.watch = (userStoryId) -> + url = $urls.resolve("userstory-watch", userStoryId) + return $http.post(url) + + service.unwatch = (userStoryId) -> + url = $urls.resolve("userstory-unwatch", userStoryId) + return $http.post(url) + service.bulkUpdateBacklogOrder = (projectId, data) -> url = $urls.resolve("bulk-update-us-backlog-order") params = {project_id: projectId, bulk_stories: data} diff --git a/app/coffee/modules/resources/webhooklogs.coffee b/app/coffee/modules/resources/webhooklogs.coffee index 02d9eced..a7db8514 100644 --- a/app/coffee/modules/resources/webhooklogs.coffee +++ b/app/coffee/modules/resources/webhooklogs.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: webhooklogs.coffee +### + resourceProvider = ($repo, $urls, $http) -> service = {} diff --git a/app/coffee/modules/resources/webhooks.coffee b/app/coffee/modules/resources/webhooks.coffee index 06907c32..d24ca488 100644 --- a/app/coffee/modules/resources/webhooks.coffee +++ b/app/coffee/modules/resources/webhooks.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: webhooks.coffee +### + resourceProvider = ($repo, $urls, $http) -> service = {} diff --git a/app/coffee/modules/resources/wiki.coffee b/app/coffee/modules/resources/wiki.coffee index 11e54bc8..c3f2c733 100644 --- a/app/coffee/modules/resources/wiki.coffee +++ b/app/coffee/modules/resources/wiki.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/search.coffee b/app/coffee/modules/search.coffee index 91a974a5..57066946 100644 --- a/app/coffee/modules/search.coffee +++ b/app/coffee/modules/search.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -64,12 +64,16 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) promise.then null, @.onInitialDataError.bind(@) # Search input watcher - @scope.searchTerm = "" + @scope.searchTerm = null loadSearchData = debounceLeading(100, (t) => @.loadSearchData(t)) + bindOnce @scope, "projectId", (projectId) => + if !@scope.searchResults && @scope.searchTerm + @.loadSearchData() + @scope.$watch "searchTerm", (term) => - if term - loadSearchData(term) + if term != undefined && @scope.projectId + @.loadSearchData(term) loadFilters: -> defered = @q.defer() @@ -84,21 +88,31 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.taskStatusById = groupBy(project.task_statuses, (x) -> x.id) @scope.severityById = groupBy(project.severities, (x) -> x.id) @scope.priorityById = groupBy(project.priorities, (x) -> x.id) - @scope.membersById = groupBy(project.memberships, (x) -> x.user) @scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id) return project - loadSearchData: (term) -> - promise = @rs.search.do(@scope.projectId, term).then (data) => - @scope.searchResults = data - return data + loadSearchData: (term = "") -> + @scope.loading = true - return promise + @._loadSearchData(term).then (data) => + if data + @scope.searchResults = data + @scope.loading = false + + _loadSearchData: (term = "") -> + @.deferredAbort.resolve() if @.deferredAbort + + @.deferredAbort = @q.defer() + + @rs.search.do(@scope.projectId, term).then (data) => + @.deferredAbort.resolve(data) + + return @.deferredAbort.promise loadInitialData: -> return @.loadProject().then (project) => @scope.projectId = project.id - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) module.controller("SearchController", SearchController) @@ -162,13 +176,22 @@ module.directive("tgSearchBox", SearchBoxDirective) SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> linkTable = ($scope, $el, $attrs, $ctrl) -> + applyAutoTab = true + activeSectionName = "userstories" tabsDom = $el.find("section.search-filter") - lastSeatchResults = null + lastSearchResults = null getActiveSection = (data) -> maxVal = 0 - selectedSectionName = null - selectedSectionData = null + selectedSection = {} + selectedSection.name = "userstories" + selectedSection.value = [] + + if !applyAutoTab + selectedSection.name = activeSectionName + selectedSection.value = data[activeSectionName] + + return selectedSection if data for name in ["userstories", "issues", "tasks", "wikipages"] @@ -176,14 +199,14 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> if value.length > maxVal maxVal = value.length - selectedSectionName = name - selectedSectionData = value + selectedSection.name = name + selectedSection.value = value break; if maxVal == 0 - return {name: "userstories", value: []} + return selectedSection - return {name:selectedSectionName, value: selectedSectionData} + return selectedSection renderFilterTabs = (data) -> for name, value of data @@ -195,6 +218,9 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> tabsDom.find("a.active").removeClass("active") tabsDom.find("li.#{section.name} a").addClass("active") + applyAutoTab = false + activeSectionName = section.name + templates = { issues: $templatecache.get("search-issues") tasks: $templatecache.get("search-tasks") @@ -218,21 +244,26 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> $el.find(".search-result-table").html(element) $scope.$watch "searchResults", (data) -> - lastSeatchResults = data + lastSearchResults = data + + return if !lastSearchResults + activeSection = getActiveSection(data) + renderFilterTabs(data) + renderTableContent(activeSection) markSectionTabActive(activeSection) $scope.$watch "searchTerm", (searchTerm) -> - $location.search("text", searchTerm) if searchTerm + $location.search("text", searchTerm) if searchTerm != undefined $el.on "click", ".search-filter li > a", (event) -> event.preventDefault() target = angular.element(event.currentTarget) sectionName = target.parent().data("name") - sectionData = lastSeatchResults[sectionName] + sectionData = if !lastSearchResults then [] else lastSearchResults[sectionName] section = { name: sectionName, diff --git a/app/coffee/modules/taskboard.coffee b/app/coffee/modules/taskboard.coffee index 5ec30653..3b5d85f1 100644 --- a/app/coffee/modules/taskboard.coffee +++ b/app/coffee/modules/taskboard.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/taskboard/charts.coffee b/app/coffee/modules/taskboard/charts.coffee index 2e50389d..a552ead5 100644 --- a/app/coffee/modules/taskboard/charts.coffee +++ b/app/coffee/modules/taskboard/charts.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/taskboard/lightboxes.coffee b/app/coffee/modules/taskboard/lightboxes.coffee index dbb97bd1..a2ce0de1 100644 --- a/app/coffee/modules/taskboard/lightboxes.coffee +++ b/app/coffee/modules/taskboard/lightboxes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index ed4ebe61..9e7f5f37 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -135,7 +135,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$emit('project:loaded', project) - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) return project @@ -184,6 +184,15 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) if @scope.usTasks[task.user_story]? and @scope.usTasks[task.user_story][task.status]? @scope.usTasks[task.user_story][task.status].push(task) + if tasks.length == 0 + + if @scope.userstories.length > 0 + usId = @scope.userstories[0].id + else + usId = null + + @scope.usTasks[usId][@scope.taskStatusList[0].id].push({isPlaceholder: true}) + return tasks loadTaskboard: -> diff --git a/app/coffee/modules/taskboard/sortable.coffee b/app/coffee/modules/taskboard/sortable.coffee index 57f99711..50337c05 100644 --- a/app/coffee/modules/taskboard/sortable.coffee +++ b/app/coffee/modules/taskboard/sortable.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/tasks.coffee b/app/coffee/modules/tasks.coffee index 2adeb9a4..1dfe6770 100644 --- a/app/coffee/modules/tasks.coffee +++ b/app/coffee/modules/tasks.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/tasks/detail.coffee b/app/coffee/modules/tasks/detail.coffee index c3b522ba..d1adbf0c 100644 --- a/app/coffee/modules/tasks/detail.coffee +++ b/app/coffee/modules/tasks/detail.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -23,6 +23,7 @@ taiga = @.taiga mixOf = @.taiga.mixOf groupBy = @.taiga.groupBy +bindMethods = @.taiga.bindMethods module = angular.module("taigaTasks") @@ -50,6 +51,8 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @log, @appMetaService, @navUrls, @analytics, @translate) -> + bindMethods(@) + @scope.taskRef = @params.taskref @scope.sectionName = @translate.instant("TASK.SECTION_NAME") @.initializeEventHandlers() @@ -77,13 +80,10 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) initializeEventHandlers: -> @scope.$on "attachment:create", => @analytics.trackEvent("attachment", "create", "create attachment on task", 1) - @rootscope.$broadcast("object:updated") - @scope.$on "attachment:edit", => - @rootscope.$broadcast("object:updated") - @scope.$on "attachment:delete", => - @rootscope.$broadcast("object:updated") @scope.$on "custom-attributes-values:edit", => @rootscope.$broadcast("object:updated") + @scope.$on "comment:new", => + @.loadTask() initializeOnDeleteGoToUrl: -> ctx = {project: @scope.project.slug} @@ -107,7 +107,6 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$emit('project:loaded', project) @scope.statusList = project.task_statuses @scope.statusById = groupBy(project.task_statuses, (x) -> x.id) - @scope.membersById = groupBy(project.memberships, (x) -> x.user) return project loadTask: -> @@ -146,9 +145,53 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) loadInitialData: -> promise = @.loadProject() return promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @.loadTask().then(=> @q.all([@.loadSprint(), @.loadUserStory()])) + ### + # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button. + # See app/modules/components/vote-button for more info + ### + onUpvote: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.upvote(@scope.taskId).then(onSuccess, onError) + + onDownvote: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.downvote(@scope.taskId).then(onSuccess, onError) + + ### + # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button. + # See app/modules/components/watch-button for more info + ### + onWatch: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.watch(@scope.taskId).then(onSuccess, onError) + + onUnwatch: -> + onSuccess = => + @.loadTask() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.tasks.unwatch(@scope.taskId).then(onSuccess, onError) + module.controller("TaskDetailController", TaskDetailController) @@ -199,7 +242,7 @@ module.directive("tgTaskStatusDisplay", ["$tgTemplate", "$compile", TaskStatusDi ## Task status button directive ############################################################################# -TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $compile, $translate) -> +TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $compile, $translate, $template) -> # Display the status of Task and you can edit it. # # Example: @@ -210,21 +253,7 @@ TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $co # - scope.statusById object # - $scope.project.my_permissions - template = _.template(""" - - """) + template = $template.get("us/us-status-button.html", true) link = ($scope, $el, $attrs, $model) -> isEditable = -> @@ -292,7 +321,7 @@ TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $co } module.directive("tgTaskStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQqueue", - "$compile", "$translate", TaskStatusButtonDirective]) + "$compile", "$translate", "$tgTemplate", TaskStatusButtonDirective]) TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading, $qqueue, $compile) -> diff --git a/app/coffee/modules/team.coffee b/app/coffee/modules/team.coffee index 5e23d3bc..7f39aa58 100644 --- a/app/coffee/modules/team.coffee +++ b/app/coffee/modules/team.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/team/main.coffee b/app/coffee/modules/team/main.coffee index 6175b035..229f652c 100644 --- a/app/coffee/modules/team/main.coffee +++ b/app/coffee/modules/team/main.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -70,30 +70,18 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.filtersRole = null loadMembers: -> - currentUser = @auth.getUser() - - if currentUser? and not currentUser.photo? - currentUser.photo = "/images/unnamed.png" - - memberships = @projectService.project.toJS().memberships - - @scope.currentUser = _.find memberships, (membership) => - return currentUser? and membership.user == currentUser.id + user = @auth.getUser() + # Calculate totals @scope.totals = {} + for member in @scope.activeUsers + @scope.totals[member.id] = 0 - _.forEach memberships, (membership) => - @scope.totals[membership.user] = 0 + # Get current user + @scope.currentUser = _.find(@scope.activeUsers, {id: user?.id}) - @scope.memberships = _.filter memberships, (membership) => - if membership.user && (not currentUser? or membership.user != currentUser.id) - return membership - - @scope.memberships = _.filter memberships, (membership) => return membership.is_active - - for membership in @scope.memberships - if not membership.photo? - membership.photo = "/images/unnamed.png" + # Get member list without current user + @scope.memberships = _.reject(@scope.activeUsers, {id: user?.id}) loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => @@ -115,10 +103,10 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) total = _.reduce(vals, (sum, el) -> sum + el) @scope.totals[userId] = total - @scope.stats = @.processStats(stats) + @scope.stats = @._processStats(stats) @scope.stats.totals = @scope.totals - processStat: (stat) -> + _processStat: (stat) -> max = _.max(stat) min = _.min(stat) singleStat = _.map stat, (value, key) -> @@ -130,17 +118,16 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) singleStat = _.object(singleStat) return singleStat - processStats: (stats) -> + _processStats: (stats) -> for key,value of stats - stats[key] = @.processStat(value) + stats[key] = @._processStat(value) return stats loadInitialData: -> promise = @.loadProject() return promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @.loadMembers() - return @.loadMemberStats() module.controller("TeamController", TeamController) @@ -230,16 +217,16 @@ LeaveProjectDirective = ($repo, $confirm, $location, $rs, $navurls, $translate) leave_project_text = $translate.instant("TEAM.ACTION_LEAVE_PROJECT") confirm_leave_project_text = $translate.instant("TEAM.CONFIRM_LEAVE_PROJECT") - $confirm.ask(leave_project_text, confirm_leave_project_text).then (finish) => + $confirm.ask(leave_project_text, confirm_leave_project_text).then (response) => promise = $rs.projects.leave($attrs.projectid) promise.then => - finish() + response.finish() $confirm.notify("success") $location.path($navurls.resolve("home")) promise.then null, (response) -> - finish() + response.finish() $confirm.notify('error', response.data._error_message) return { diff --git a/app/coffee/modules/user-settings.coffee b/app/coffee/modules/user-settings.coffee index d2c8f150..c40a8825 100644 --- a/app/coffee/modules/user-settings.coffee +++ b/app/coffee/modules/user-settings.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/user-settings/change-password.coffee b/app/coffee/modules/user-settings/change-password.coffee index bfb856f9..ebd9ce5f 100644 --- a/app/coffee/modules/user-settings/change-password.coffee +++ b/app/coffee/modules/user-settings/change-password.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -91,4 +91,4 @@ UserChangePasswordDirective = ($rs, $confirm, $loading, $translate) -> link:link } -module.directive("tgUserChangePassword", ["$tgResources", "$tgConfirm", "$tgLoading", UserChangePasswordDirective]) +module.directive("tgUserChangePassword", ["$tgResources", "$tgConfirm", "$tgLoading", "$translate", UserChangePasswordDirective]) diff --git a/app/coffee/modules/user-settings/lightboxes.coffee b/app/coffee/modules/user-settings/lightboxes.coffee index 5a7d6ccc..c3dd5375 100644 --- a/app/coffee/modules/user-settings/lightboxes.coffee +++ b/app/coffee/modules/user-settings/lightboxes.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/user-settings/main.coffee b/app/coffee/modules/user-settings/main.coffee index 151cc2dc..297431e4 100644 --- a/app/coffee/modules/user-settings/main.coffee +++ b/app/coffee/modules/user-settings/main.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -57,6 +57,7 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin) @location.replace() @scope.lang = @getLan() + @scope.theme = @getTheme() maxFileSize = @config.get("maxUploadFileSize", null) if maxFileSize @@ -68,6 +69,8 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin) promise.then null, @.onInitialDataError.bind(@) loadInitialData: -> + @scope.availableThemes = @config.get("themes", []) + return @rs.locales.list().then (locales) => @scope.locales = locales return locales @@ -79,6 +82,11 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin) return @scope.user.lang || @translate.preferredLanguage() + getTheme: -> + return @scope.user.theme || + @config.get("defaultTheme") || + "taiga" + module.controller("UserSettingsController", UserSettingsController) @@ -96,6 +104,7 @@ UserProfileDirective = ($confirm, $auth, $repo, $translate) -> changeEmail = $scope.user.isAttributeModified("email") $scope.user.lang = $scope.lang + $scope.user.theme = $scope.theme onSuccess = (data) => $auth.setUser(data) diff --git a/app/coffee/modules/user-settings/nav.coffee b/app/coffee/modules/user-settings/nav.coffee index a1ad7cc0..c235961d 100644 --- a/app/coffee/modules/user-settings/nav.coffee +++ b/app/coffee/modules/user-settings/nav.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/user-settings/notifications.coffee b/app/coffee/modules/user-settings/notifications.coffee index bef96385..df74e003 100644 --- a/app/coffee/modules/user-settings/notifications.coffee +++ b/app/coffee/modules/user-settings/notifications.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/userstories.coffee b/app/coffee/modules/userstories.coffee index ddf38d8b..711afcfc 100644 --- a/app/coffee/modules/userstories.coffee +++ b/app/coffee/modules/userstories.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee index 7dcc7ab0..3af67d91 100644 --- a/app/coffee/modules/userstories/detail.coffee +++ b/app/coffee/modules/userstories/detail.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -24,6 +24,7 @@ taiga = @.taiga mixOf = @.taiga.mixOf groupBy = @.taiga.groupBy bindOnce = @.taiga.bindOnce +bindMethods = @.taiga.bindMethods module = angular.module("taigaUserStories") @@ -50,6 +51,8 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @log, @appMetaService, @navUrls, @analytics, @translate) -> + bindMethods(@) + @scope.usRef = @params.usref @scope.sectionName = @translate.instant("US.SECTION_NAME") @.initializeEventHandlers() @@ -92,6 +95,9 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$on "attachment:create", => @analytics.trackEvent("attachment", "create", "create attachment on userstory", 1) + @scope.$on "comment:new", => + @.loadUs() + initializeOnDeleteGoToUrl: -> ctx = {project: @scope.project.slug} @scope.onDeleteGoToUrl = @navUrls.resolve("project", ctx) @@ -112,7 +118,6 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.statusList = project.us_statuses @scope.statusById = groupBy(project.us_statuses, (x) -> x.id) @scope.taskStatusById = groupBy(project.task_statuses, (x) -> x.id) - @scope.membersById = groupBy(project.memberships, (x) -> x.user) @scope.pointsList = _.sortBy(project.points, "order") @scope.pointsById = groupBy(@scope.pointsList, (e) -> e.id) return project @@ -177,9 +182,53 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) loadInitialData: -> promise = @.loadProject() return promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @.loadUs().then(=> @q.all([@.loadSprint(), @.loadTasks()])) + ### + # Note: This methods (onUpvote() and onDownvote()) are related to tg-vote-button. + # See app/modules/components/vote-button for more info + ### + onUpvote: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.upvote(@scope.usId).then(onSuccess, onError) + + onDownvote: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.downvote(@scope.usId).then(onSuccess, onError) + + ### + # Note: This methods (onWatch() and onUnwatch()) are related to tg-watch-button. + # See app/modules/components/watch-button for more info + ### + onWatch: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.watch(@scope.usId).then(onSuccess, onError) + + onUnwatch: -> + onSuccess = => + @.loadUs() + @rootscope.$broadcast("object:updated") + onError = => + @confirm.notify("error") + + return @rs.userstories.unwatch(@scope.usId).then(onSuccess, onError) + module.controller("UserStoryDetailController", UserStoryDetailController) diff --git a/app/coffee/modules/wiki.coffee b/app/coffee/modules/wiki.coffee index cae93384..76c0c615 100644 --- a/app/coffee/modules/wiki.coffee +++ b/app/coffee/modules/wiki.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/coffee/modules/wiki/main.coffee b/app/coffee/modules/wiki/main.coffee index 847b342d..c108954a 100644 --- a/app/coffee/modules/wiki/main.coffee +++ b/app/coffee/modules/wiki/main.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -86,7 +86,6 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) - @scope.membersById = groupBy(project.memberships, (x) -> x.user) return project loadWiki: -> @@ -119,7 +118,7 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) loadInitialData: -> promise = @.loadProject() return promise.then (project) => - @.fillUsersAndRoles(project.users, project.roles) + @.fillUsersAndRoles(project.members, project.roles) @q.all([@.loadWikiLinks(), @.loadWiki()]).then () => @@ -127,15 +126,15 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) title = @translate.instant("WIKI.DELETE_LIGHTBOX_TITLE") message = @scope.wikiTitle - @confirm.askOnDelete(title, message).then (finish) => + @confirm.askOnDelete(title, message).then (askResponse) => onSuccess = => - finish() + askResponse.finish() ctx = {project: @scope.projectSlug} @location.path(@navUrls.resolve("project-wiki", ctx)) @confirm.notify("success") onError = => - finish(false) + askResponse.finish(false) @confirm.notify("error") @repo.remove(@scope.wiki).then onSuccess, onError @@ -158,7 +157,7 @@ WikiSummaryDirective = ($log, $template, $compile, $translate) -> user = $scope.usersById[wiki.last_modifier] if user is undefined - user = {name: "unknown", imgUrl: "/images/unnamed.png"} + user = {name: "unknown", imgUrl: "/images/user-noimage.png"} else user = {name: user.full_name_display, imgUrl: user.photo} @@ -236,8 +235,6 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $ onError = -> $confirm.notify("error") - console.log $el.find('.save-container') - currentLoading = $loading() .removeClasses("icon-floppy") .target($el.find('.icon-floppy')) @@ -251,6 +248,13 @@ EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $ promise.finally -> currentLoading.finish() + $el.on "click", "a", (event) -> + target = angular.element(event.target) + href = target.attr('href') + if href.indexOf("#") == 0 + event.preventDefault() + $('body').scrollTop($(href).offset().top) + $el.on "mousedown", ".view-wiki-content", (event) -> target = angular.element(event.target) return if not isEditable() diff --git a/app/coffee/modules/wiki/nav.coffee b/app/coffee/modules/wiki/nav.coffee index 15d46dd0..ef7901a0 100644 --- a/app/coffee/modules/wiki/nav.coffee +++ b/app/coffee/modules/wiki/nav.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -83,17 +83,17 @@ WikiNavDirective = ($tgrepo, $log, $location, $confirm, $navUrls, $analytics, $l title = $translate.instant("WIKI.DELETE_LIGHTBOX_TITLE") message = $scope.wikiLinks[linkId].title - $confirm.askOnDelete(title, message).then (finish) => + $confirm.askOnDelete(title, message).then (askResponse) => promise = $tgrepo.remove($scope.wikiLinks[linkId]) promise.then -> promise = $ctrl.loadWikiLinks() promise.then -> - finish() + askResponse.finish() render($scope.wikiLinks) promise.then null, -> - finish() + askResponse.finish() promise.then null, -> - finish(false) + askResponse.finish(false) $confirm.notify("error") $el.on "keyup", ".new input", (event) -> diff --git a/app/coffee/utils.coffee b/app/coffee/utils.coffee index 621f09aa..f0706a86 100644 --- a/app/coffee/utils.coffee +++ b/app/coffee/utils.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -185,6 +185,13 @@ defineImmutableProperty = (obj, name, fn) => return fn_result } +_.mixin + removeKeys: (obj, keys) -> + _.chain([keys]).flatten().reduce( + (obj, key) -> + delete obj[key]; obj + , obj).value() + taiga = @.taiga taiga.nl2br = nl2br taiga.bindMethods = bindMethods diff --git a/app/fonts/OpenSans-Light.woff b/app/fonts/OpenSans-Light.woff new file mode 100644 index 00000000..d50b69a1 --- /dev/null +++ b/app/fonts/OpenSans-Light.woff @@ -0,0 +1,811 @@ + + + + + + + + + + + + open-sans/OpenSans-Light.woff at gh-pages · FontFaceKit/open-sans + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content +
+ + + + + + + + + + + + +
+
+
+ +
+
+
+ + +
    + +
  • +
    + +
    + + + + Watch + + + + +
    + +
    +
    +
    +
  • + +
  • + +
    + +
    + + +
    +
    + + +
    + +
  • + +
  • + + + Fork + + + + +
  • + +
+ +

+ + /open-sans + + + + + +

+
+
+ +
+
+
+ + + +
+ +
+

HTTPS clone URL

+
+ + + + +
+
+ + +
+

SSH clone URL

+
+ + + + +
+
+ + +
+

Subversion checkout URL

+
+ + + + +
+
+ + + +
You can clone with +
,
, or
. + + + +
+ + + + + + + Download ZIP + +
+
+ +
+ + + + + + + +
+ +
+ + + branch: + gh-pages + + + +
+ +
+ + + + +
+ + +
+ + +
+
+ + Martin Domke + + +
+ + + +
+ +
+
+
+ +
+ Raw + History +
+ + + +
+ +
+ +
+ 62.844 kB +
+
+ +
+
+ View Raw +
+
+ +
+ +Jump to Line + + +
+ +
+ +
+
+ + +
+ +
+ +
+ + +
+
+
+ +
+
+
+
+
+ +
+ + + + + + +
+ + + Something went wrong with that request. Please try again. +
+ + + + + + + + + diff --git a/app/fonts/taiga.eot b/app/fonts/taiga.eot index f8cdfc90..b130ed12 100644 Binary files a/app/fonts/taiga.eot and b/app/fonts/taiga.eot differ diff --git a/app/fonts/taiga.svg b/app/fonts/taiga.svg index 435c7a92..b3b37a2a 100644 --- a/app/fonts/taiga.svg +++ b/app/fonts/taiga.svg @@ -7,57 +7,55 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/taiga.ttf b/app/fonts/taiga.ttf index 67c906cc..a766a2ec 100644 Binary files a/app/fonts/taiga.ttf and b/app/fonts/taiga.ttf differ diff --git a/app/fonts/taiga.woff b/app/fonts/taiga.woff index 5c346f3a..6606a61a 100644 Binary files a/app/fonts/taiga.woff and b/app/fonts/taiga.woff differ diff --git a/app/images/backlog-empty.png b/app/images/backlog-empty.png new file mode 100644 index 00000000..a7179c86 Binary files /dev/null and b/app/images/backlog-empty.png differ diff --git a/app/images/issues-empty.png b/app/images/issues-empty.png new file mode 100644 index 00000000..4668e027 Binary files /dev/null and b/app/images/issues-empty.png differ diff --git a/app/images/search-empty.png b/app/images/search-empty.png new file mode 100644 index 00000000..d95477dc Binary files /dev/null and b/app/images/search-empty.png differ diff --git a/app/images/sprint-empty.png b/app/images/sprint-empty.png new file mode 100644 index 00000000..e51329a4 Binary files /dev/null and b/app/images/sprint-empty.png differ diff --git a/app/images/user-noimage.png b/app/images/user-noimage.png new file mode 100644 index 00000000..d779bbd4 Binary files /dev/null and b/app/images/user-noimage.png differ diff --git a/app/index.jade b/app/index.jade index 105ca868..a9994cef 100644 --- a/app/index.jade +++ b/app/index.jade @@ -10,7 +10,7 @@ html(lang="en") meta(name="description", content="Taiga is a project management platform for startups and agile developers & designers who want a simple, beautiful tool that makes work truly enjoyable.") meta(name="keywords", content="agile, scrum, taiga, management, project, developer, designer, user experience") //-meta(name="viewport", content="width=device-width, user-scalable=no") - link(rel="stylesheet", href="/styles/main.css") + link(rel="stylesheet", href="/styles/theme-taiga.css") link(rel="icon", type="image/png", href="/images/favicon.png") //- PRERENDER SERVICE: This is to know when the page is completely loaded. @@ -37,6 +37,8 @@ html(lang="en") include partials/includes/components/notification-message + div(tg-joy-ride) + script(src="/js/libs.js?v=#{v}") script(src="/js/templates.js?v=#{v}") script(src="/js/app-loader.js?v=#{v}") diff --git a/app/locales/locale-ca.json b/app/locales/locale-ca.json index af5b4123..72fb43d1 100644 --- a/app/locales/locale-ca.json +++ b/app/locales/locale-ca.json @@ -6,7 +6,7 @@ "LOADING_PROJECT": "Carregant projecte...", "DATE": "DD MMM YYYY", "DATETIME": "DD MMM YYYY HH:mm", - "SAVE": "Guardar", + "SAVE": "Desa", "CANCEL": "Següent", "ACCEPT": "Acceptar", "DELETE": "Esborrar", @@ -32,22 +32,23 @@ "ONE_ITEM_LINE": "In item per línia", "NEW_BULK": "Nova inserció en grup", "RELATED_TASKS": "Tasques relacionades", - "LOGOUT": "Logout", + "LOGOUT": "Surt", "EXTERNAL_USER": "un usuari extern", "GENERIC_ERROR": "Un Oompa Loompas diu {{error}}.", "IOCAINE_TEXT": "Un poc saturat per una tasca? Fes-ho saber als teus companys clicant a Iocaina quan edites la tasca. Es possible ser inmune a aquesta (fictícia) poció mortal consumint xicotetes dòsis poc a poc, així com es possible millorar amb xicotets nous desafiaments!", + "CAPSLOCK_WARNING": "Be careful! You're writing with capital letters and this input is case sensitive.", "FORM_ERRORS": { "DEFAULT_MESSAGE": "Aquest valor pareix invàlid.", "TYPE_EMAIL": "Deu ser un correu vàlid.", "TYPE_URL": "Deu ser una URL vàlida.", "TYPE_URLSTRICT": "Deu ser una URL vàlida.", "TYPE_NUMBER": "Deu ser un nombre vàlid.", - "TYPE_DIGITS": "This value should be digits.", - "TYPE_DATEISO": "This value should be a valid date (YYYY-MM-DD).", - "TYPE_ALPHANUM": "This value should be alphanumeric.", - "TYPE_PHONE": "This value should be a valid phone number.", - "NOTNULL": "This value should not be null.", - "NOT_BLANK": "This value should not be blank.", + "TYPE_DIGITS": "Aquest valor ha de ser numèric", + "TYPE_DATEISO": "Aquest valor ha de ser una data vàlida (YYYY-MM-DD)", + "TYPE_ALPHANUM": "Aquest valor ha de ser alfanumèric.", + "TYPE_PHONE": "Aquest valor ha de ser un número de telèfon vàlid.", + "NOTNULL": "Aquest valor no pot ser nul.", + "NOT_BLANK": "Aquest valor no pot estar buit", "REQUIRED": "Aquest valor és necessari.", "REGEXP": "Aquest valor pareix invàlid.", "MIN": "This value should be greater than or equal to %s.", @@ -57,8 +58,8 @@ "MAX_LENGTH": "This value is too long. It should have %s characters or less.", "RANGE_LENGTH": "This value length is invalid. It should be between %s and %s characters long.", "MIN_CHECK": "You must select at least %s choices.", - "MAX_CHECK": "You must select %s choices or less.", - "RANGE_CHECK": "You must select between %s and %s choices.", + "MAX_CHECK": "Heu de seleccionar la opció %s o inferior.", + "RANGE_CHECK": "Heu de seleccionar entre les opcions %s i %s.", "EQUAL_TO": "Aquest valor hauria de ser el mateix." }, "PICKERDATE": { @@ -100,10 +101,10 @@ "SAT": "Ds" } }, - "SEE_USER_PROFILE": "See {{username }} profile", + "SEE_USER_PROFILE": "Veure el perfil de {{usuari}}", "USER_STORY": "Història d'usuari", - "TASK": "Task", - "ISSUE": "Issue", + "TASK": "Tasca", + "ISSUE": "incidència", "TAGS": { "PLACEHOLDER": "Afegir tag", "DELETE": "Elimina l'etiqueta", @@ -130,7 +131,8 @@ "POINTS": "Punts", "BLOCKED_NOTE": "Nota de bloqueig", "IS_BLOCKED": "està bloquejat", - "REF": "Ref" + "REF": "Ref", + "VOTES": "Vots" }, "ROLES": { "ALL": "Tot" @@ -148,11 +150,27 @@ "OPEN": "Obert" }, "WATCHERS": { - "ADD": "Afegir seguidor", - "TITLE": "Seguidors", + "ADD": "Add watchers", + "TITLE_ADD": "Add a project member to the watchers list", "DELETE": "Esborrar seguidor", "TITLE_LIGHTBOX_DELETE_WARTCHER": "Esborrar seguidor..." }, + "WATCH_BUTTON": { + "WATCH": "Watch", + "WATCHING": "Observant", + "UNWATCH": "Unwatch", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Watch/Unwatch this item", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Upvoted", + "DOWNVOTE": "Downvote", + "VOTERS": "Voters", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "Camps personalitzats", "SAVE": "Salva camp personalitzat", @@ -222,9 +240,9 @@ "ISSUES": { "NAME": "Incidències", "VIEW_ISSUES": "Vore incidències", - "ADD_ISSUES": "Afegir incidències", - "MODIFY_ISSUES": "Modificar incidències", - "DELETE_ISSUES": "Esborrar incidències" + "ADD_ISSUES": "Afegeix incidències", + "MODIFY_ISSUES": "Modifica incidències", + "DELETE_ISSUES": "Elimina incidències" }, "WIKI": { "NAME": "Wiki", @@ -250,13 +268,13 @@ "INVITED_YOU": "T'ha convidat a participar en el projecte", "NOT_REGISTERED_YET": "No t'has registrat encara?", "REGISTER": "Registrat", - "CREATE_ACCOUNT": "Crea el teu conter gratis ací" + "CREATE_ACCOUNT": "crea el teu compte gratuït ací" }, "LOGIN_COMMON": { - "HEADER": "Ja tinc un conter de Taiga", + "HEADER": "Ja tinc un compte de Taiga", "PLACEHOLDER_AUTH_NAME": "Nom d'usuari i correu electrònic (sensible a majúscules i minúscules)", "LINK_FORGOT_PASSWORD": "L'has oblidat?", - "TITLE_LINK_FORGOT_PASSWORD": "Has oblidat la teua contrasenya?", + "TITLE_LINK_FORGOT_PASSWORD": "Has oblidat la teva contrasenya?", "ACTION_ENTER": "Entrar", "ACTION_SIGN_IN": "Entrar", "PLACEHOLDER_AUTH_PASSWORD": "Contrasenya (sensible a majúscules i minúscules)" @@ -270,11 +288,11 @@ "PAGE_DESCRIPTION": "Crea un nou compte de Taiga, una plataforma de gestió de projectes per a empreses àgils que necessiten una eïna sencilla i elegant que et permet gaudir del teu treball" }, "REGISTER_FORM": { - "TITLE": "Registra un nou compte de Taiga (gratis)", + "TITLE": "Registra un nou compte de Taiga (gratuït)", "PLACEHOLDER_NAME": "Trieu un nom d'usuari (sensible a majúscules i minúscules)", "PLACEHOLDER_FULL_NAME": "Escriu el teu nom complet", "PLACEHOLDER_EMAIL": "El teu correu", - "PLACEHOLDER_PASSWORD": "Tria una contrasenya(sensible a majúscules i minúscules)", + "PLACEHOLDER_PASSWORD": "Defineix una contrasenya (sensible a majúscules i minúscules)", "ACTION_SIGN_UP": "Registrar-se", "TITLE_LINK_LOGIN": "Entrar", "LINK_LOGIN": "Ja estàs registrat? Entra" @@ -284,17 +302,17 @@ "PAGE_DESCRIPTION": "Escriviu el vostre nom d'usuari o correu electrònic per a conseguir-ne un de nou" }, "FORGOT_PASSWORD_FORM": { - "TITLE": "Oops, has oblidat la teua contrasenya?", + "TITLE": "Oops, has oblidat la teva contrasenya?", "SUBTITLE": "Escriviu el vostre nom d'usuari o correu electrònic per a conseguir-ne un de nou", "PLACEHOLDER_FIELD": "Nom d'usuari o correu electrònic", "ACTION_RESET_PASSWORD": "Resetejar contrasenya", - "LINK_CANCEL": "No, portam enrere, crec que ho recorde.", + "LINK_CANCEL": "No, porta'm enrere, crec que ja ho recordo.", "SUCCESS": "Mira el teu correu!
Hem enviat un correu amb les instrucciones per a setejar una nova contrasenya.", "ERROR": "Segons els nostres Oompa Loompas, no estàs registrat encara." }, "CHANGE_PASSWORD": { "PAGE_TITLE": "Canvi de contrasenya - Taiga", - "PAGE_DESCRIPTION": "Crea un nou password per Taiga i... hey! Menja un poc més de ferro, es bo pel cervell :P ", + "PAGE_DESCRIPTION": "Estableix una contrasenya nova per al teu compte de Taiga i per cer cert! Potser voleu menjar més aliments rics en ferro, és bo per al cervell :P", "SECTION_NAME": "Canvi de contrasenya", "FIELD_CURRENT_PASSWORD": "Contrasenya actual", "PLACEHOLDER_CURRENT_PASSWORD": "La teua contrasenya actua (buit si no tens contrasenya encara)", @@ -305,14 +323,12 @@ "ERROR_PASSWORD_MATCH": "Les contrasenyes no coincideixen" }, "CHANGE_PASSWORD_RECOVERY_FORM": { - "TITLE": "Crea un nou password", - "SUBTITLE": "Menja un poc més de ferro, es bo pel cervell :P ", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Recuperar token de contrasenya", - "LINK_NEED_TOKEN": "Neccesites un?", - "TITLE_LINK_NEED_TOKEN": "Necessites un token per a recuperar la teua contrasenya perque l'has oblidat?", + "TITLE": "Crea una nova contrasenya", + "SUBTITLE": "Menja més aliments rics en ferro, es bo pel cervell :P ", "PLACEHOLDER_NEW_PASSWORD": "nova contrasenya", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Reescriu la nova contrasenya", "ACTION_RESET_PASSWORD": "Resetejar contrasenya", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", "SUCCESS": "Els Oompa Loompas han salvat la teua contrasenya
Prova a entrar amb ella." }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "Accepta la invitació a un projecte en Taiga, una plataforma de gestió de projectes per a empreses àgils que necessiten una eïna sencilla i elegant que et permet gaudir del teu treball" }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Ooops, ha hagut un problema
Els nosters Oompa Loompas no troben la teua invitació.", + "NOT_FOUND": "Els Oompa Loompas no troben la teua invitació.", "SUCCESS": "T'has incorporat a este projecte. Vos donem la benvinguda a {{project_name}}", "ERROR": "Segons els nostres OOmpa Loompas, no estàs registrat encara o has escrit una contrasenya invàlida." }, "HOME": { "PAGE_TITLE": "Home - Taiga", - "PAGE_DESCRIPTION": "La home de Taiga amb els teus projectes principals, totes les històries d'usuari, tasques e incidències que tens assignades i aquelles que estàs Watching", - "EMPTY_WATCHING": "Segueix els projectes, històries d'usuari, tasques e incidències... de les que vols conèixer la evolució.", + "PAGE_DESCRIPTION": "La home de Taiga amb els teus projectes principals, totes les històries d'usuari, tasques i incidències que tens assignades i aquelles que estàs Watching", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", "EMPTY_PROJECT_LIST": "No tens cap projecte encara", "WORKING_ON_SECTION": "En Progrés", "WATCHING_SECTION": "Observant" @@ -343,11 +360,11 @@ "DESCRIPTION": "Escriu una descripció curta", "DEPRECATED": "(obsolet)", "DEPRECATED_FILE": "Obsolet?", - "ADD": "Afegir nou adjunt. <%- maxFileSizeMsg %>", + "ADD": "Afegeix un nou adjunt. {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Max. grandària: {{maxFileSize}}]", "SHOW_DEPRECATED": "+ mostra els adjunts obsolets", "HIDE_DEPRECATED": "- Amagar els adjunts obsolets", - "COUNT_DEPRECATED": "({{ counter }} deprecated)", + "COUNT_DEPRECATED": "({{ counter }} obsolet)", "MAX_UPLOAD_SIZE": "El tamany màxim de pujada es ", "DATE": "DD MMM YYYY [at] hh:mm", "ERROR_UPLOAD_ATTACHMENT": "No hem pogut pujar '{{fileName}}'. {{errorMessage}}", @@ -374,7 +391,7 @@ }, "MEMBERSHIPS": { "TITLE": "Gestió de Membres", - "PAGE_TITLE": "Memberships - {{projectName}}", + "PAGE_TITLE": "Afiliacions - {{projectName}}", "ADD_BUTTON": "+ Nou membre", "ADD_BUTTON_TITLE": "Afegir nou membre" }, @@ -407,15 +424,20 @@ "MEETUP": "Trobar-se", "MEETUP_DESCRIPTION": "Tria el teu sistema de videonconferéncia. Inclosos els desenvolupadors necessitan contacte cara a cara", "SELECT_VIDEOCONFERENCE": "Selecciona un sistema de videconferència", - "SALT_CHAT_ROOM": "Pots afegir un code salt a la sala de xat" + "SALT_CHAT_ROOM": "Pots afegir un code salt a la sala de xat", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Personalitzat", + "URL_CHAT_ROOM": "Adreça web del teu xat" }, "PROJECT_PROFILE": { - "PAGE_TITLE": "{{sectionName}} - Project profile - {{projectName}}", + "PAGE_TITLE": "{{sectionName}} - Perfil de projecte - {{projectName}}", "PROJECT_DETAILS": "Detalls de projecte", "PROJECT_NAME": "Nom del projecte", "PROJECT_SLUG": "Slug de projecte", - "NUMBER_SPRINTS": "Nombre de sprints", - "NUMBER_US_POINTS": "Nombre de punts de US", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", "TAGS": "Etiquetes", "DESCRIPTION": "Descripció", "PUBLIC_PROJECT": "Projecte públic", @@ -425,13 +447,13 @@ "REPORTS": { "TITLE": "Informes", "SUBTITLE": "Exporta les dades del teu projecte en CSV i fes els teus propis informes.", - "DESCRIPTION": "Descarrega el fitxer CSV o copia la URL generada o obri-la amb el teu editor de text o fulla de càlcul per a fer els teus propis informes. Podràs visualitzar i analitzar les dades fàcilment.", + "DESCRIPTION": "Download a CSV file or copy the generated URL and open it in your favourite text editor or spreadsheet to make your own project data reports. You will be able to visualize and analyze all your data easily.", "HELP": "Com utilitzar açó a la meu fulla de càlcul?", "REGENERATE_TITLE": "Canviar URL", "REGENERATE_SUBTITLE": "Vas a canviar la URL d'accés al CSV. La URL previa no funcionarà. Estàs segur?" }, "CSV": { - "SECTION_TITLE_US": "user stories reports", + "SECTION_TITLE_US": "informes d'històries d'usuari", "SECTION_TITLE_TASK": "infome de tasques", "SECTION_TITLE_ISSUE": "informe d'incidències", "DOWNLOAD": "Descarrega el CSV", @@ -442,13 +464,16 @@ }, "CUSTOM_FIELDS": { "TITLE": "Camps personalitzats", - "SUBTITLE": "Especifica els camps personalitzats del les teues históries d'usuari, tasques e incidències", + "SUBTITLE": "Especifiqueu els camps personalitzats del les vostres històries d'usuari, tasques i incidències", "US_DESCRIPTION": "Camps personalitzats d'històries d'usuari", "US_ADD": "Afegeix camps personalitzats en històries d'usuari", "TASK_DESCRIPTION": "Camps personalitzats de tasques", "TASK_ADD": "Afegix camps personalitzats en tasques", "ISSUE_DESCRIPTION": "Camps personalitzats d'incidències", - "ISSUE_ADD": "Afegix camps personalitzats en incidències" + "ISSUE_ADD": "Afegix camps personalitzats en incidències", + "FIELD_TYPE_TEXT": "Text", + "FIELD_TYPE_MULTI": "Múltiples línies", + "FIELD_TYPE_DATE": "Data" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - Project values - {{projectName}}", @@ -456,26 +481,26 @@ "ERROR_DELETE_ALL": "No pots esborrar tots els valors." }, "PROJECT_VALUES_POINTS": { - "TITLE": "Points", + "TITLE": "Punts", "SUBTITLE": "Especifica els punts en els que poden ser estimades les històries d'usuari", - "US_TITLE": "US points", + "US_TITLE": "Punts d'història d'usuari", "ACTION_ADD": "Afegir punts nous" }, "PROJECT_VALUES_PRIORITIES": { - "TITLE": "Priorities", + "TITLE": "Prioritats", "SUBTITLE": "Especifica les prioritats que tindran les teues tasques", - "ISSUE_TITLE": "Issue priorities", - "ACTION_ADD": "Add new priority" + "ISSUE_TITLE": "Prioritats d'incidències", + "ACTION_ADD": "Afegir nova prioritat" }, "PROJECT_VALUES_SEVERITIES": { - "TITLE": "Severities", - "SUBTITLE": "Especifica les severitats que tindran les teues incidències", - "ISSUE_TITLE": "Issue severities", - "ACTION_ADD": "Add new severity" + "TITLE": "Severitats", + "SUBTITLE": "Especifiqueu les severitats que tindran les vostres incidències", + "ISSUE_TITLE": "Severitats d'incidències", + "ACTION_ADD": "Afegir nova severitat" }, "PROJECT_VALUES_STATUS": { "TITLE": "Estat", - "SUBTITLE": "Especifica els estats del les teues históries d'usuari, tasques e incidències", + "SUBTITLE": "Especifica els estats de les vostres històries d'usuari, tasques i incidències", "US_TITLE": "Estats d'US", "TASK_TITLE": "Estats de tasques", "ISSUE_TITLE": "Estats d'incidències" @@ -487,9 +512,12 @@ "ACTION_ADD": "Afegir now {{objName}}" }, "ROLES": { - "PAGE_TITLE": "Roles - {{projectName}}", + "PAGE_TITLE": "Rols - {{projectName}}", "WARNING_NO_ROLE": "Ves amb compte, cap rol en el teu projecte pot estimar punts per a les històries d'usuari", "HELP_ROLE_ENABLED": "Si està activat, els membres assignats a aquest rol podràn estimar els punts d'històries d'usuaris", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", "COUNT_MEMBERS": "{{ role.members_count }} membres amb aquest rol", "TITLE_DELETE_ROLE": "Esborrar rol", "REPLACEMENT_ROLE": "Tots els usuaris amb aquest rol es canviaran a", @@ -543,7 +571,7 @@ "WEBHOOK_NAME": "Webhook '{{name}}'" }, "CUSTOM_ATTRIBUTES": { - "PAGE_TITLE": "{{sectionName}} - Custom Attributes - {{projectName}}", + "PAGE_TITLE": "{{sectionName}} - Atributs personalitzats - {{projectName}}", "ADD": "Afegix camp personalitzat", "EDIT": "Edita el camp personalitzat", "DELETE": "Esborrar camp personalitzat", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "Actiu", "STATUS_PENDING": "Pendent", "DELETE_MEMBER": "Esborrar membre", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "Hem tornat a enviar la invitació a '{{email}}'.", "ERROR_SEND_INVITATION": "No s'ha enviat la invitació", "SUCCESS_DELETE": "Hem esborrat {{message}}.", @@ -624,32 +653,56 @@ "USER": { "PROFILE": { "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", - "EDIT": "Editar perfil", - "FOLLOW": "Follow", + "EDIT": "Edita el perfil", + "FOLLOW": "Seguir", + "CLOSED_US": "HU tancada", "PROJECTS": "Projectes", - "CLOSED_US": "Closed US", - "CONTACTS": "Contacts", - "REPORT": "Report Abuse", - "ACTIVITY_TAB": "Activity Tab", - "PROJECTS_TAB": "Projects Tab", - "CONTACTS_TAB": "Contacts Tab", - "FAVORITES_TAB": "Favorites Tab", - "CONTACTS_EMPTY": "{{username}} doesn't have contacts yet", - "CURRENT_USER_CONTACTS_EMPTY": "You don't have contacts yet", - "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "The people with whom you work at Taiga will be your contacts automatically", - "PROJECTS_EMPTY": "{{username}} doesn't' have projects yet" + "PROJECTS_EMPTY": "{{username}} encara no te cap projecte", + "CONTACTS": "Contactes", + "CONTACTS_EMPTY": "{{Usuari}} no té contactes encara", + "CURRENT_USER_CONTACTS_EMPTY": "No tens contactes encara", + "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Les persones amb les que treballa a Taiga seran els seus contactes de forma automàtica", + "REPORT": "Informar d'un abús", + "TABS": { + "ACTIVITY_TAB": "Activitat", + "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "PROJECTS_TAB": "Projectes", + "PROJECTS_TAB_TITLE": "List all projects that this user is a member", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Vots", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Watched", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Contactes", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } }, "PROFILE_SIDEBAR": { "TITLE": "El teu perfil", "DESCRIPTION": "La gent pot vore tot el que fas i en qué estàs treballant. Afegix una bio interessant per a donar una millor versió de la teua informació.", - "ADD_INFO": "Edita la teua bio" + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Type something...", + "FILTER_TYPE_ALL": "Tot", + "FILTER_TYPE_ALL_TITLE": "Show all", + "FILTER_TYPE_PROJECTS": "Projectes", + "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", + "FILTER_TYPE_TASKS": "Tasques", + "FILTER_TYPE_TASK_TITLES": "Show only tasks", + "FILTER_TYPE_ISSUES": "Incidències", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." } }, "PROJECT": { "PAGE_TITLE": "{{projectName}}", "WELCOME": "Benvinguts", "SECTION_PROJECTS": "Projectes", - "HELP": "Reorder your projects to set in the top the most used ones.
The top 10 projects will appear in the top navigation bar project list", + "HELP": "Reordena els teus projectes per a establir els més utilitzats en les primeres posicions.
Els 10 millors projectes apareixeran en la llista de projectes de la barra de navegació superior", "PRIVATE": "Projecte privat", "STATS": { "PROJECT": "punts
projecte", @@ -672,32 +725,32 @@ "SECTION_TITLE": "Els teus projectes", "PLACEHOLDER_SEARCH": "Cerca en...", "ACTION_CREATE_PROJECT": "Crear projecte", - "ACTION_IMPORT_PROJECT": "Import project", - "SEE_MORE_PROJECTS": "See more projects", - "TITLE_CREATE_PROJECT": "Create project", - "TITLE_IMPORT_PROJECT": "Import project", + "ACTION_IMPORT_PROJECT": "Importar projecte", + "SEE_MORE_PROJECTS": "Visualitza més projectes", + "TITLE_CREATE_PROJECT": "Crear projecte", + "TITLE_IMPORT_PROJECT": "Importar projecte", "TITLE_PRVIOUS_PROJECT": "Mostra projectes previs", "TITLE_NEXT_PROJECT": "Mostrar próxims projectes", - "HELP_TITLE": "Taiga Support Page", - "HELP": "Help", - "FEEDBACK_TITLE": "Enviar sugerències", - "FEEDBACK": "Sugerències", - "NOTIFICATIONS_TITLE": "Edit your notification settings", + "HELP_TITLE": "Pàgina d'ajuda de Taiga", + "HELP": "Ajuda", + "FEEDBACK_TITLE": "Enviar suggerències", + "FEEDBACK": "Suggerències", + "NOTIFICATIONS_TITLE": "Edita la configuració de les teves notificacions", "NOTIFICATIONS": "Notificacions", "ORGANIZATIONS_TITLE": "Edit your organizations", - "ORGANIZATIONS": "Edit organizations", - "SETTINGS_TITLE": "Edit your settings", - "SETTINGS": "Settings", - "VIEW_PROFILE_TITLE": "View Profile", - "VIEW_PROFILE": "View Profile", - "EDIT_PROFILE_TITLE": "Edit your profile", - "EDIT_PROFILE": "Edit Profile", + "ORGANIZATIONS": "Editar organitzacions", + "SETTINGS_TITLE": "Editeu les vostres preferències", + "SETTINGS": "Preferències", + "VIEW_PROFILE_TITLE": "Visualitza el perfil", + "VIEW_PROFILE": "Visualitza el perfil", + "EDIT_PROFILE_TITLE": "Editeu el vostre perfil", + "EDIT_PROFILE": "Edita el perfil", "CHANGE_PASSWORD_TITLE": "Canvi de contrasenya", "CHANGE_PASSWORD": "Canvi de contrasenya", - "DASHBOARD_TITLE": "Dashboard", + "DASHBOARD_TITLE": "Tauler", "DISCOVER_TITLE": "Discover trending projects", - "DISCOVER": "Discover", - "ACTION_REORDER": "Drag & drop to reorder" + "DISCOVER": "Descobreix", + "ACTION_REORDER": "Arrossega els elements per endreçar" }, "IMPORT": { "TITLE": "Important Projecte", @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "Els Oompa Loompas tenen problemes important les teues dades: {{error_message}}", "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) es massa gran per als nostres Oompa Loompas, prova amb algun inferior a ({{maxFileSize}})", "SYNC_SUCCESS": "El teu projecte s'ha importat correctament" + }, + "LIKE_BUTTON": { + "LIKE": "Like", + "LIKED": "Liked", + "UNLIKE": "Unlike", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Watch", + "WATCHING": "Observant", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Receive all notifications", + "NOTIFY_ALL_TITLE": "Receive all notifications for this project", + "NOTIFY_INVOLVED": "Only involved", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Unwatch", + "UNWATCH_TITLE": "Unwatch this project" + } } }, "LIGHTBOX": { @@ -740,7 +814,7 @@ "FEEDBACK": { "TITLE": "Contans...", "COMMENT": "...un bug, una sugerència, algo bonic... inclús el teu pitjor malson amb Taiga.", - "ACTION_SEND": "Enviar sugerències" + "ACTION_SEND": "Enviar suggerències" }, "SEARCH": { "TITLE": "Cerca", @@ -778,13 +852,13 @@ } }, "US": { - "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}", - "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}", - "SECTION_NAME": "User story details", + "PAGE_TITLE": "{{userStorySubject}} - Història d'Usuari {{userStoryRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Estat: {{userStoryStatus }}. Completat {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} de {{userStoryTotalTasks}} tasques tancades). Punts: {{userStoryPoints}}. Descripció: {{userStoryDescription}}", + "SECTION_NAME": "Detalls de la història d'usuari", "LINK_TASKBOARD": "Panell de tasques", "TITLE_LINK_TASKBOARD": "Anar a panell de tasques", - "TOTAL_POINTS": "total", - "ADD": "+ Afegir nova història d'usuari", + "TOTAL_POINTS": "total points", + "ADD": "+ Afegeix una nova història d'usuari", "ADD_BULK": "Afegeix noves històries d'usuari en grup", "PROMOTED": "Aquesta US ha sigut promocionada desde:", "TITLE_LINK_GO_TO_ISSUE": "Anar a la incidència", @@ -868,8 +942,12 @@ }, "BACKLOG": { "PAGE_TITLE": "Backlog - {{projectName}}", - "PAGE_DESCRIPTION": "The backlog panel, with user stories and sprints of the project {{projectName}}: {{projectDescription}}", + "PAGE_DESCRIPTION": "El panell Backlog, amb històries d'usuari i esprints del projecte {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Backlog", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Admin", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "Envia al Sprint", "SHOW_FILTERS": "Mostra filtres", "SHOW_TAGS": "Mostra etiquetes", @@ -889,13 +967,13 @@ "CHART": { "XAXIS_LABEL": "Sprints", "YAXIS_LABEL": "Punts", - "OPTIMAL": "Punts pendent òptims per sprint {{xval}} deuría ser {{yval}}", - "REAL": "Punts pendent reals per sprint {{xval}} es {{yval}}", - "INCREMENT_TEAM": "Punts incrementats per requeriment de l'equip per sprint {{xval}} és {{yval}}", - "INCREMENT_CLIENT": "Punts incrementat per requeriment de client {{xval}} és {{yval}}" + "OPTIMAL": "Els punts pendents òptims per l'esprint \"{{sprintName}}\" haurien de ser {{value}}", + "REAL": "Real pending points for sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_TEAM": "Incremented points by team requirements for sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_CLIENT": "Incremented points by client requirements for sprint \"{{sprintName}}\" is {{value}}" }, "TAGS": { - "TOGGLE": "Toggle tags visibility", + "TOGGLE": "Alterna la visibilitat d'etiquetes", "SHOW": "Mostra etiquetes", "HIDE": "Amaga etiquetes" }, @@ -909,7 +987,8 @@ "OPEN_TASKS": "tasques
obertes", "CLOSED_TASKS": "tasques
tancades", "IOCAINE_DOSES": "dosis
iocaína", - "SHOW_STATISTICS_TITLE": "Mostrar estadístiques" + "SHOW_STATISTICS_TITLE": "Mostrar estadístiques", + "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph" }, "SUMMARY": { "PROJECT_POINTS": "punts
projecte", @@ -918,10 +997,10 @@ "POINTS_PER_SPRINT": "punts
sprint" }, "FILTERS": { - "TOGGLE": "Toggle filters visibility", + "TOGGLE": "Alterna la visibilitat dels filtres", "TITLE": "Filtres", "REMOVE": "Esborra filtres", - "HIDE": "Hide Filters", + "HIDE": "Amaga filtres", "SHOW": "Mostra filtres", "FILTER_CATEGORY_STATUS": "Estats", "FILTER_CATEGORY_TAGS": "Etiquetes" @@ -932,29 +1011,32 @@ "LINK_TASKBOARD": "Anar al panell de sprint", "TITLE_LINK_TASKBOARD": "Anar al panell de {{::name}}", "NUMBER_SPRINTS": "
sprints", - "TITLE_ACTION_NEW_SPRINT": "+ Nou sprint", - "ACTION_NEW_SPRINT": "+ Nou sprint", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", "ACTION_SHOW_CLOSED_SPRINTS": "Mostra sprints tancats", "ACTION_HIDE_CLOSED_SPRINTS": "Amaga sprints tancats" } }, "ERROR": { "TEXT1": "Ha passat algo i els nostres Oompa Loompas estàn arreglant-ho.", - "TEXT2": "Prova a recarregar de nou", "NOT_FOUND": "No trobat", "NOT_FOUND_TEXT": "Error 404. La pàgina que busques no existeix. Pots tornar a la página principal de taiga i provar desde allí.", "PERMISSION_DENIED": "Permis denegat", - "PERMISSION_DENIED_CODE": "Error 403", + "PERMISSION_DENIED_TEXT": "You don't have permission to access to this page.", "VERSION_ERROR": "Algú dins de Taiga ha canviat aȯ abans i els Oompa Loompas no pode aplicar els teus canvis. Per favor recarrega i aplica els teus canvis (es perdràn)" }, "TASKBOARD": { - "PAGE_TITLE": "{{sprintName}} - Sprint taskboard - {{projectName}}", + "PAGE_TITLE": "{{sprintName}} - Panell de tasques del esprint - {{projectName}}", "PAGE_DESCRIPTION": "Sprint {{sprintName}} (from {{startDate}} to {{endDate}}) of {{projectName}}. Completed {{completedPercentage}}% ({{completedPoints}} of {{totalPoints}} points). {{openTasks}} opened tasks of {{totalTasks}}.", "SECTION_NAME": "Panell de tasques", "TITLE_ACTION_ADD": "Afegir nova tasca", "TITLE_ACTION_ADD_BULK": "Afegeix noves històries d'usuari en grup", "TITLE_ACTION_ASSIGN": "Assignar tasca", "TITLE_ACTION_EDIT": "Editar tasca", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", "TABLE": { "COLUMN": "Història d'usuari", "TITLE_ACTION_FOLD": "Plegar columna", @@ -974,7 +1056,7 @@ }, "TASK": { "PAGE_TITLE": "{{taskSubject}} - Task {{taskRef}} - {{projectName}}", - "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Description: {{taskDescription}}", + "PAGE_DESCRIPTION": "Estat: {{taskStatus }}. Descripció: {{taskDescription}}", "SECTION_NAME": "Detalls de la tasca", "LINK_TASKBOARD": "Panell de tasques", "TITLE_LINK_TASKBOARD": "Anar a panell de tasques", @@ -998,7 +1080,7 @@ "TITLE_ACTION_IOCAINE": "Un poc saturat per una tasca? Fes-ho saber als teus companys clicant a Iocaina quan edites la tasca. Es possible ser inmune a aquesta (fictícia) poció mortal consumint xicotetes dòsis poc a poc, així com es possible millorar amb xicotets nous desafiaments!" }, "NOTIFICATION": { - "OK": "Tot està ok", + "OK": "Tot està bé", "WARNING": "Oops, ha passat algo...", "WARNING_TEXT": "Els teus canvis no s'han salvat!", "SAVED": "Els Oompa Loompas han salvat els teus canvis!", @@ -1021,10 +1103,10 @@ "SUCCESS": "Els Oompa Loompas han actualitzat el teu correu" }, "ISSUES": { - "PAGE_TITLE": "Issues - {{projectName}}", + "PAGE_TITLE": "Incidències - {{projectName}}", "PAGE_DESCRIPTION": "The issues list panel of the project {{projectName}}: {{projectDescription}}", "LIST_SECTION_NAME": "Incidències", - "SECTION_NAME": "Issue details", + "SECTION_NAME": "Detalls d'incidència", "ACTION_NEW_ISSUE": "+ NOVA INCIDÈNCIA", "ACTION_PROMOTE_TO_US": "Promocionar història d'usuari", "PLACEHOLDER_FILTER_NAME": "Escriu el filtre i pressiona Intro", @@ -1073,41 +1155,44 @@ "SEVERITY": "Severitat", "PRIORITY": "Prioritat", "SUBJECT": "Descripció", + "VOTES": "Vots", "STATUS": "Estats", "CREATED": "Creat", "ASSIGNED_TO": "Assignat a" }, "TITLE_ACTION_CHANGE_STATUS": "Canvia l'estat", "TITLE_ACTION_ASSIGNED_TO": "Assignat a", + "BLOCKED": "Bloquejat", "EMPTY": { "TITLE": "No hi ha incidències a reportar :-)", - "SUBTITLE": "Has trobat una incidència?", - "ACTION_CREATE_ISSUE": "Crea nova incidència" + "SUBTITLE": "Has trobat una incidència?" } } }, "ISSUE": { - "PAGE_TITLE": "{{issueSubject}} - Issue {{issueRef}} - {{projectName}}", - "PAGE_DESCRIPTION": "Status: {{issueStatus }}. Type: {{issueType}}, Priority: {{issuePriority}}. Severity: {{issueSeverity}}. Description: {{issueDescription}}" + "PAGE_TITLE": "{{issueSubject}} - incidència {{issueRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Estat: {{issueStatus }}. Tipus: {{issueType}}, Prioritat: {{issuePriority}}. Severitat: {{issueSeverity}}. Descripció: {{issueDescription}}" }, "KANBAN": { "PAGE_TITLE": "Kanban - {{projectName}}", - "PAGE_DESCRIPTION": "The kanban panel, with user stories of the project {{projectName}}: {{projectDescription}}", + "PAGE_DESCRIPTION": "El panell Kanban, amb històries d'usuari del projecte {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Kanban", "TITLE_ACTION_FOLD": "Plegar columna", "TITLE_ACTION_UNFOLD": "Desplegar columna", "TITLE_ACTION_FOLD_CARDS": "Plegar targeta", "TITLE_ACTION_UNFOLD_CARDS": "Desplegar targetes", - "TITLE_ACTION_ADD_US": "Afegir història d'usuari", + "TITLE_ACTION_ADD_US": "Afegeix una nova història d'usuari", "TITLE_ACTION_ADD_BULK": "Afegir en grup", "ACTION_SHOW_ARCHIVED": "Mostrar arxivats", "ACTION_HIDE_ARCHIVED": "Amagar arxivats", "HIDDEN_USER_STORIES": "Les històries d'usuari en aquest estats estan amagades", "ARCHIVED": "Has arxivat", - "UNDO_ARCHIVED": "Arrastra de nou per desfer" + "UNDO_ARCHIVED": "Arrastra de nou per desfer", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" }, "SEARCH": { - "PAGE_TITLE": "Search - {{projectName}}", + "PAGE_TITLE": "Cerca - {{projectName}}", "PAGE_DESCRIPTION": "Search anything, user stories, issues, tasks or wiki pages, in the project {{projectName}}: {{projectDescription}}", "FILTER_USER_STORIES": "Històries d'usuari", "FILTER_ISSUES": "Incidències", @@ -1119,7 +1204,7 @@ "EMPTY_DESCRIPTION": "Prova amb una de les pestanyes o busca de nou" }, "TEAM": { - "PAGE_TITLE": "Team - {{projectName}}", + "PAGE_TITLE": "Equip - {{projectName}}", "PAGE_DESCRIPTION": "The team panel to show all the members of the project {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Equip", "APP_TITLE": "EQUIP - {{projectName}}", @@ -1163,7 +1248,7 @@ "USER_PROFILE": "Perfil d'usuari", "CHANGE_PASSWORD": "Canvi de contrasenya", "NOTIFICATIONS": "Notificacions", - "FEEDBACK": "Sugerències", + "FEEDBACK": "Suggerències", "TITLE_AVATAR": "Preferències d'usuari" } }, @@ -1182,7 +1267,9 @@ "BIO": "Bio (max. 210 caràcters)", "PLACEHOLDER_BIO": "Contans algo sobre tu mateix", "LANGUAGE": "Idioma", - "LANGUAGE_DEFAULT": "-- utiliza l'idioma per defecte --" + "LANGUAGE_DEFAULT": "-- utiliza l'idioma per defecte --", + "THEME": "Tema", + "THEME_DEFAULT": "-- utilitza el tema per defecte --" } }, "WIZARD": { @@ -1213,10 +1300,10 @@ "HINTS": { "SECTION_NAME": "Hint", "LINK": "If you want to know how to use it visit our support page", - "LINK_TITLE": "Visit our support page", - "HINT1_TITLE": "Did you know you can import and export projects?", - "HINT1_TEXT": "This allow you to extract all your data from one Taiga and move it to another one.", - "HINT2_TITLE": "Did you know you can create custom fields?", + "LINK_TITLE": "Visita la nostra pàgina d'ajuda", + "HINT1_TITLE": "Saps que pots importar i exportar projectes?", + "HINT1_TEXT": "Això li permet extreure totes les dades d'un Taiga i moure'l a un altre.", + "HINT2_TITLE": "Saps que pots crear camps personalitzats?", "HINT2_TEXT": "Teams can now create custom fields as a flexible means to enter specific data useful for their particular workflow", "HINT3_TITLE": "Reorder your projects to feature those most relevant to you", "HINT3_TEXT": "The top 10 project will be in your top bar direct access", @@ -1225,27 +1312,100 @@ }, "TIMELINE": { "UPLOAD_ATTACHMENT": "{{username}} has uploaded a new attachment in {{obj_name}}", - "US_CREATED": "{{username}} has created a new US {{obj_name}} in {{project_name}}", - "ISSUE_CREATED": "{{username}} has created a new issue {{obj_name}} in {{project_name}}", - "TASK_CREATED": "{{username}} has created a new task {{obj_name}} in {{project_name}}", - "TASK_CREATED_WITH_US": "{{username}} has created a new task {{obj_name}} in {{project_name}} which belongs to the US {{us_name}}", + "US_CREATED": "{{username}} ha creat una nova història d'usuari {{obj_name}} a {{project_name}}", + "ISSUE_CREATED": "{{username}} ha creat una nova incidència {{obj_name}} a {{project_name}}", + "TASK_CREATED": "{{username}} ha creat una nova tasca {{obj_name}} a {{project_name}}", + "TASK_CREATED_WITH_US": "{{username}} ha creat una nova tasca {{obj_name}} a {{project_name}} provinent de US {{us_name}}", "WIKI_CREATED": "{{username}} has created a new wiki page {{obj_name}} in {{project_name}}", "MILESTONE_CREATED": "{{username}} has created a new sprint {{obj_name}} in {{project_name}}", - "NEW_PROJECT": "{{username}} created the project {{project_name}}", + "NEW_PROJECT": "{{username}} ha creat el projecte {{project_name}}", "MILESTONE_UPDATED": "{{username}} has updated the sprint {{obj_name}}", "US_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}} to {{new_value}}", + "US_UPDATED_POINTS": "{{username}} has updated '{{role_name}}' points of the US {{obj_name}} to {{new_value}}", "ISSUE_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the issue {{obj_name}}", - "TASK_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}}", - "TASK_UPDATED_WITH_US": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}}", - "WIKI_UPDATED": "{{username}} has updated the wiki page {{obj_name}}", - "NEW_COMMENT_US": "{{username}} has commented in the US {{obj_name}}", - "NEW_COMMENT_ISSUE": "{{username}} has commented in the issue {{obj_name}}", - "NEW_COMMENT_TASK": "{{username}} has commented in the task {{obj_name}}", - "NEW_MEMBER": "{{project_name}} has a new member", - "US_ADDED_MILESTONE": "{{username}} has added the US {{obj_name}} to {{sprint_name}}", - "US_REMOVED_FROM_MILESTONE": "{{username}} has added the US {{obj_name}} to the backlog", - "BLOCKED": "{{username}} has blocked {{obj_name}}", - "UNBLOCKED": "{{username}} has unblocked {{obj_name}}", - "NEW_USER": "{{username}} has joined Taiga" + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the issue {{obj_name}} to {{new_value}}", + "TASK_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} to {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} to {{new_value}}", + "TASK_UPDATED_WITH_US": "{{username}} ha actualitzat l'atribut \"{{field_name}}\" de la tasca {{obj_name}} de la història d'usuari {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} ha actualitzat l'atribut \"{{field_name}}\" de la tasca {{obj_name}} de la història d'usuari {{us_name}} amb el valor {{new_value}}", + "WIKI_UPDATED": "{{username}} ha actualitzat la pàgina wiki {{obj_name}}", + "NEW_COMMENT_US": "{{username}} ha comentat la història d'usuari {{obj_name}}", + "NEW_COMMENT_ISSUE": "{{username}} ha comentat la incidència {{obj_name}}", + "NEW_COMMENT_TASK": "{{username}} ha comentat la tasca {{obj_name}}", + "NEW_MEMBER": "{{project_name}} te un nou membre", + "US_ADDED_MILESTONE": "{{username}} ha afegit la història d'usuari {{obj_name}} a {{sprint_name}}", + "US_MOVED": "{{username}} ha mogut la història d'usuari {{obj_name}}", + "US_REMOVED_FROM_MILESTONE": "{{username}} ha afegit la història d'usuari {{obj_name}} al backlog", + "BLOCKED": "{{username}} ha bloquejat {{obj_name}}", + "UNBLOCKED": "{{username}} ha desbloquejat {{obj_name}}", + "NEW_USER": "{{username}} s'ha unit a Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "Una aplicació externa requereix autenticació", + "PAGE_DESCRIPTION": "Una aplicació externa requereix autenticació", + "AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?", + "LOGIN_WITH_ANOTHER_USER": "Identifica't amb un altre nom d'usuari", + "AUTHORIZE_APP": "Autoritza l'aplicació", + "CANCEL": "Següent" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "El teu projecte", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "En Progrés", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Observant", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "Històries d'usuari", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } } } \ No newline at end of file diff --git a/app/locales/locale-de.json b/app/locales/locale-de.json index ad5c2de0..803a8a61 100644 --- a/app/locales/locale-de.json +++ b/app/locales/locale-de.json @@ -16,7 +16,7 @@ "EDIT": "Bearbeiten", "DRAG": "Ziehen", "TAG_LINE": "Ihr agiles, freies und quelloffenes Projekt-Management Tool", - "TAG_LINE_2": "MIR GEFÄLLT DAS PROJEKT", + "TAG_LINE_2": "LIEBE DEIN PROJEKT", "BLOCK": "Blockieren", "UNBLOCK": "Blockierung aufheben", "BLOCKED": "Blockiert", @@ -36,6 +36,7 @@ "EXTERNAL_USER": "ein externer Benutzer", "GENERIC_ERROR": "Eins unserer Helferlein sagt {{error}}.", "IOCAINE_TEXT": "Fühlen Sie sich von einer Aufgabe etwas erdrückt? Stellen Sie sicher, dass andere davon erfahren, indem Sie auf Locaine klicken, wenn Sie eine Aufgabe ändern. Es ist möglich, gegen dieses (fiktive) tödliche Gift immun zu werden, indem man kleine Mengen über einen Zeitraum hinweg einnimmt. Genauso, wie es möglich ist, besser in dem zu werden, was man tut, indem man gelegentlich zusätzliche Herausforderungen annimmt! ", + "CAPSLOCK_WARNING": "Achtung! SIe schreiben in Großbuchstaben und bei dieser Eingabe ist Groß- und Kleinschreibung zu beachten", "FORM_ERRORS": { "DEFAULT_MESSAGE": "Dieser Wert scheint ungültig zu sein. ", "TYPE_EMAIL": "Dieser Wert sollte eine gültige E-Mail Adresse enthalten.", @@ -130,7 +131,8 @@ "POINTS": "Punkte", "BLOCKED_NOTE": "Blockierungsgrund", "IS_BLOCKED": "wird blockiert", - "REF": "Ref" + "REF": "Ref", + "VOTES": "Stimmen" }, "ROLES": { "ALL": "Alle" @@ -149,10 +151,26 @@ }, "WATCHERS": { "ADD": "Beobachter hinzufügen", - "TITLE": "Beobachter", + "TITLE_ADD": "Add a project member to the watchers list", "DELETE": "Beobachter löschen", "TITLE_LIGHTBOX_DELETE_WARTCHER": "Beobachter löschen..." }, + "WATCH_BUTTON": { + "WATCH": "Beobachten", + "WATCHING": "Beobachtet", + "UNWATCH": "Nicht beobachten", + "WATCHERS": "Beobachter", + "BUTTON_TITLE": "Beobachte/Beobachte dieses Objekt (nicht mehr)", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Hochgevotet", + "DOWNVOTE": "Runtergevotet", + "VOTERS": "Voters", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "Benutzerdefinierte Felder", "SAVE": "Benutzerdefiniertes Feld speichern", @@ -307,12 +325,10 @@ "CHANGE_PASSWORD_RECOVERY_FORM": { "TITLE": "Erstellen Sie einen neuen Taiga Pass", "SUBTITLE": "Und hey, es empfiehlt sich, mehr eisenreiche Nahrung zu sich zu nehmen - die ist gut für's Gehirn :P ", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Passwort Token wiederherstellen", - "LINK_NEED_TOKEN": "Benötigen Sie einen?", - "TITLE_LINK_NEED_TOKEN": "Benötigen Sie ein Token, um Ihr vergessenes Passwort zu retten?", "PLACEHOLDER_NEW_PASSWORD": "Neues Passwort", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Wiederholen Sie die Eingabe des neuen Passworts ", "ACTION_RESET_PASSWORD": "Passwort zurücksetzen", + "ERROR": "Unsere Helferelein haben Probleme deine Anfrage zum Zurücksetzen des Passworts zu finden. Probiere es einfach nochmal.", "SUCCESS": "Unsere Oompa Loompas haben das neue Passwort gespeichert.
Hier gehts zum login." }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "Nehmen Sie die Einladung an und treten Sie einem Projekt in Taiga bei, einer Projekt-Management Plattform für Neugründer und agile Entwickler und Designer, die ein unkompliziertes und ansprechendes Tool möchten, das die Arbeit wirklich angenehm macht. " }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Huch, wir haben ein Problem
Unsere Helferlein können Ihre Einladung nicht finden.", + "NOT_FOUND": "Unsere Helferlein können deine Einladung nicht finden.", "SUCCESS": "Sie sind diesem Projekt erfolgreich beigetreten. Herzlich willkommen bei {{project_name}}", "ERROR": "Laut unseren Helferlein sind Sie noch nicht registriert, oder Sie haben ein ungültiges Passwort eingegeben. " }, "HOME": { "PAGE_TITLE": "Home - Taiga", "PAGE_DESCRIPTION": "Die Taiga Homepage mit Ihren wichtigsten Projekten und all Ihren zugeordneten und beobachteten User-Stories, Aufgaben und Tickets. ", - "EMPTY_WATCHING": "Folgen Sie den Projekten, User-Stories, Aufgaben, Tickets... über die Sie etwas wissen möchten :)", + "EMPTY_WORKING_ON": "Hier sieht’s ziemlich leer aus, oder? Beginne deine Arbeit mit Taiga und wir zeigen dir hier die Stories, Tasks und Issues an denen du arbeitest.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", "EMPTY_PROJECT_LIST": "Sie haben noch keine Projekte", "WORKING_ON_SECTION": "Zuletzt bearbeitet", "WATCHING_SECTION": "Beobachtet" @@ -343,7 +360,7 @@ "DESCRIPTION": "Geben Sie eine kurze Beschreibung ein", "DEPRECATED": "(verworfen)", "DEPRECATED_FILE": "Verworfen?", - "ADD": "Neuen Anhang hinzufügen. <%- maxFileSizeMsg %>", + "ADD": "Neuen Anhang hinzufügen. {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Max. Größe: {{maxFileSize}}]", "SHOW_DEPRECATED": "+ verworfene Anhänge zeigen", "HIDE_DEPRECATED": "- verworfene Anhänge verbergen", @@ -407,15 +424,20 @@ "MEETUP": "Zusammentreffen", "MEETUP_DESCRIPTION": "Wählen Sie Ihr Videokonferenzsystem. Auch Entwickler brauchen Kontakt von Angesicht zu Angesicht. ", "SELECT_VIDEOCONFERENCE": "Wählen Sie ein Videokonferenzsystem", - "SALT_CHAT_ROOM": "Wenn Sie möchten, können Sie einen Salt Code zum Namen des Chatrooms hinzufügen" + "SALT_CHAT_ROOM": "Wenn Sie möchten, können Sie einen Salt Code zum Namen des Chatrooms hinzufügen", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "Erscheint in", + "TALKY_CHAT_ROOM": "Gesprächig", + "CUSTOM_CHAT_ROOM": "Kunde", + "URL_CHAT_ROOM": "URL Ihres Chatrooms" }, "PROJECT_PROFILE": { "PAGE_TITLE": "{{sectionName}} - Projekt Profil - {{projectName}}", "PROJECT_DETAILS": "Projekt Details", "PROJECT_NAME": "Projektname", "PROJECT_SLUG": "Projekt Slug", - "NUMBER_SPRINTS": "Anzahl der Sprints", - "NUMBER_US_POINTS": "Anzahl der User-Story Punkte", + "NUMBER_SPRINTS": "Anzahl von Sprints (0 für unbegrenzte Anzahl)", + "NUMBER_US_POINTS": "Anzahl an US Points (0 für unbegrenzte Anzahl)", "TAGS": "Schlagwörter", "DESCRIPTION": "Beschreibung", "PUBLIC_PROJECT": "Öffentliches Projekt", @@ -425,7 +447,7 @@ "REPORTS": { "TITLE": "Berichte", "SUBTITLE": "Exportieren Sie Ihre Projektdaten in CSV Format und erstellen Sie Ihre eigenen Berichte. ", - "DESCRIPTION": "Laden Sie eine CSV Datei herunter oder kopieren Sie die generierte URL und öffnen Sie sie in Ihrem bevorzugten Text-Editor oder in Ihrer Tabellenkalkulation, um Ihre eigenen Projektdaten-Berichte zu erstellen. So können Sie Ihre Daten leicht überblicken und analysieren.", + "DESCRIPTION": "Laden Sie eine CSV-Datei herunter oder kopieren Sie die generierte URL und öffnen Sie sie in Ihrem Lieblingstexteditor oder Tabellenkalkulationsprogramm um Ihre eigenen Projektdaten Berichte zu erstellen. So können Sie Ihre Daten einfach visualisieren und analysieren.", "HELP": "Wie kann ich dies in meiner eigenen Tabellenkalkulation nutzen?", "REGENERATE_TITLE": "Die URL ändern", "REGENERATE_SUBTITLE": "Sie sind im Begriff, die CSV data access URL zu ändern. Die vorherige URL wird deaktiviert. Sind Sie sicher?" @@ -448,7 +470,10 @@ "TASK_DESCRIPTION": "Aufgaben benutzerdefinierte Felder", "TASK_ADD": "Fügen Sie ein benutzerdefiniertes Feld bei Aufgaben hinzu", "ISSUE_DESCRIPTION": "Tickets benutzerdefinierte Felder", - "ISSUE_ADD": "Fügen Sie den Tickets ein benutzerdefiniertes Feld hinzu " + "ISSUE_ADD": "Fügen Sie den Tickets ein benutzerdefiniertes Feld hinzu ", + "FIELD_TYPE_TEXT": "Text", + "FIELD_TYPE_MULTI": "Mehrzeilig", + "FIELD_TYPE_DATE": "Datum" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - Projekt Werte - {{projectName}}", @@ -490,6 +515,9 @@ "PAGE_TITLE": "Rollen - {{projectName}}", "WARNING_NO_ROLE": "Beachten Sie, keine Rolle in Ihrem Projekt wird in der Lage sein, die Punktevergabe für User-Stories einzuschätzen. ", "HELP_ROLE_ENABLED": "Wenn Sie dies freischalten, werden Mitglieder, denen diese Rolle zugewiesen ist, in der Lage sein, die Punktevergabe für User-Stories vorzunehmen. ", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", "COUNT_MEMBERS": "{{ role.members_count }} Mitglieder mit dieser Rolle", "TITLE_DELETE_ROLE": "Rolle löschen", "REPLACEMENT_ROLE": "Alle Benutzer mit dieser Rolle werden verschoben nach ", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "Aktiv", "STATUS_PENDING": "Noch nicht erledigt", "DELETE_MEMBER": "Mitglied löschen", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "Wir haben die Einladung erneut versandt an '{{email}}'.", "ERROR_SEND_INVITATION": "Wir haben die Einladung nicht versandt. ", "SUCCESS_DELETE": "Gelöscht {{message}}.", @@ -626,23 +655,47 @@ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", "EDIT": "Profil ändern", "FOLLOW": "Folgen", - "PROJECTS": "Projekte", "CLOSED_US": "Geschlossene User-Story", + "PROJECTS": "Projekte", + "PROJECTS_EMPTY": "{{username}} besitzt noch keine Projekte", "CONTACTS": "Kontakte", - "REPORT": "Missbrauch melden ", - "ACTIVITY_TAB": "Aktivitäten Registerkarte", - "PROJECTS_TAB": "Projekte Registerkarte", - "CONTACTS_TAB": "Kontakte Registerkarte", - "FAVORITES_TAB": "Favoriten Registerkarte", "CONTACTS_EMPTY": "{{username}} hat noch keine Kontakte", "CURRENT_USER_CONTACTS_EMPTY": "Sie haben noch keine Kontakte ", "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Die Leute, mit denen Sie auf Taiga zusammenarbeiten, werden automatisch zu Ihren Kontakten ", - "PROJECTS_EMPTY": "{{username}} besitzt noch keine Projekte" + "REPORT": "Missbrauch melden ", + "TABS": { + "ACTIVITY_TAB": "Aktivität", + "ACTIVITY_TAB_TITLE": "Alle Aktivitäten dieses Benutzers anzeigen", + "PROJECTS_TAB": "Projekte", + "PROJECTS_TAB_TITLE": "Alle Projekte bei denen dieser Benutzer Mitgleid ist anzeigen", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Stimmen", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Beobachtet", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Kontakte", + "CONTACTS_TAB_TITLE": "Alle Kontakte dieses Buntzers anzeigen" + } }, "PROFILE_SIDEBAR": { "TITLE": "Ihr Profil", "DESCRIPTION": "Da andere Mitglieder sehen, dass sie ebenfalls an einem Projekt arbeiten wäre es schön, wenn Sie ein paar Informationen zu Ihrer Person angeben.", "ADD_INFO": "Bio bearbeiten" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Geben sie etwas ein...", + "FILTER_TYPE_ALL": "Alle", + "FILTER_TYPE_ALL_TITLE": "Alle anzeigen", + "FILTER_TYPE_PROJECTS": "Projekte", + "FILTER_TYPE_PROJECT_TITLES": "Nur Projekte anzeigen", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Nur User-Stories anzeigen", + "FILTER_TYPE_TASKS": "Aufgaben", + "FILTER_TYPE_TASK_TITLES": "Zeige nur Tasks", + "FILTER_TYPE_ISSUES": "Tickets", + "FILTER_TYPE_ISSUES_TITLE": "Nur Fehler anzeigen", + "EMPTY_TITLE": "Es sieht so aus, als wäre hier nichts." } }, "PROJECT": { @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "Unsere Helferlein haben Probleme beim Importieren Ihrer Datei: {{error_message}}", "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) ist zu schwierig für unsere Helferlein, versuchen Sie es bitte mit einer kleineren Datei als ({{maxFileSize}})", "SYNC_SUCCESS": "Ihr Projekt wurde erfolgreich importiert" + }, + "LIKE_BUTTON": { + "LIKE": "Gefällt mir", + "LIKED": "Gefällt mir", + "UNLIKE": "Gefällt mir nicht mehr", + "BUTTON_TITLE": "Like oder Unlike dieses Projekt", + "COUNTER_TITLE": "{total, plural, one{ein Fan} other{# Fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Beobachten", + "WATCHING": "Beobachtet", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Alle Benachrichtigungen empfangen", + "NOTIFY_ALL_TITLE": "Alle Benachrichtigungen für dieses Projekt anzeigen", + "NOTIFY_INVOLVED": "Nur Beteiligte", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Nicht beobachten", + "UNWATCH_TITLE": "Unwatch this project" + } } }, "LIGHTBOX": { @@ -783,7 +857,7 @@ "SECTION_NAME": "User-Story Details", "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Zu Taskboard wechseln", - "TOTAL_POINTS": "Total", + "TOTAL_POINTS": "Gesamtpunkte", "ADD": "+ Neue User-Story anlegen", "ADD_BULK": "Mehrere neue User-Stories hinzufügen", "PROMOTED": "Diese User-Story entstand aus Ticket: ", @@ -870,6 +944,10 @@ "PAGE_TITLE": "Auftragsliste - {{projectName}}", "PAGE_DESCRIPTION": "Das Auftragslisten-Panel mit User-Stories und Sprints des Projekts. {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Auftragsliste", + "CUSTOMIZE_GRAPH": "Personalisiere deinen Backloggraph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Administrator ", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "Zum aktuellen Sprint wechseln", "SHOW_FILTERS": "Filter zeigen", "SHOW_TAGS": "Tags anzeigen", @@ -889,10 +967,10 @@ "CHART": { "XAXIS_LABEL": "Sprints", "YAXIS_LABEL": "Punkte", - "OPTIMAL": "Optimale unerledigte Punkte für Sprint {{xval}} sollten sein {{yval}}", - "REAL": "Tatsächliche Anzahl unerledigter Punkte für Sprint {{xval}} ist {{yval}}", - "INCREMENT_TEAM": "Erhöhte Punkteanzahl von Team Anfragen für Sprint {{xval}} ist {{yval}}", - "INCREMENT_CLIENT": "Erhöhte Punkteanzahl von Kundenanfragen für Sprint {{xval}} ist {{yval}}" + "OPTIMAL": "Optimale unerledigte Punkte für Sprint \"{{sprintName}}\" sollten sein {{value}}", + "REAL": "Tatsächliche Anzahl unerledigter Punkte für Sprint \"{{sprintName}}\" ist {{value}}", + "INCREMENT_TEAM": "Erhöhte Punktezahl von Teamanfragen für Sprint \"{{sprintName}}\" ist {{value}}", + "INCREMENT_CLIENT": "Erhöhte Punkteanzahl von Kundenanfragen für Sprint \"{{sprintName}}\" ist {{value}}" }, "TAGS": { "TOGGLE": "Sichtbarkeit des Schlagwortes umschalten", @@ -909,7 +987,8 @@ "OPEN_TASKS": "offene
Aufgaben", "CLOSED_TASKS": "geschlossene
Aufgaben", "IOCAINE_DOSES": "Iocaine
Dosen", - "SHOW_STATISTICS_TITLE": "Statistik anzeigen" + "SHOW_STATISTICS_TITLE": "Statistik anzeigen", + "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph" }, "SUMMARY": { "PROJECT_POINTS": "Projekt
Punkte", @@ -932,19 +1011,20 @@ "LINK_TASKBOARD": "Sprint Taskboard", "TITLE_LINK_TASKBOARD": "Gehe zu Taskboard von \"{{name}}\"", "NUMBER_SPRINTS": "
Sprints", - "TITLE_ACTION_NEW_SPRINT": "+ Neuer Sprint", - "ACTION_NEW_SPRINT": "+ Neuer Sprint", + "EMPTY": "DU HAST KEINE SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", "ACTION_SHOW_CLOSED_SPRINTS": "Geschlossene Sprints anzeigen", "ACTION_HIDE_CLOSED_SPRINTS": "Geschlossene Sprints ausblenden" } }, "ERROR": { "TEXT1": "Es gibt ein Problem und unsere Helferlein arbeiten schon daran!", - "TEXT2": "Versuchen Sie in Kürze neu zu laden. ", "NOT_FOUND": "Nicht gefunden", "NOT_FOUND_TEXT": "Fehler 404. Die angeforderte Seite existiert nicht mehr. Möglicherweise finden Sie das Gesuchte, wenn Sie zur TAIGA Homepage zurückkehren. ", "PERMISSION_DENIED": "Berechtigung verweigert", - "PERMISSION_DENIED_CODE": "Fehler 403.", + "PERMISSION_DENIED_TEXT": "Sie haben nicht die Berechtigung um auf diese Seite zuzugreifen", "VERSION_ERROR": "Jemand anderes hat dies schon geändert und unsere Helferlein können Ihre Änderungen deshalb nicht übernehmen. Bitte laden Sie die Seite neu und machen Sie die Änderungen erneut (die aktuelle Eingabe geht dabei verloren)." }, "TASKBOARD": { @@ -955,6 +1035,8 @@ "TITLE_ACTION_ADD_BULK": "Mehrere Aufgaben hinzufügen", "TITLE_ACTION_ASSIGN": "Aufgabe zuweisen", "TITLE_ACTION_EDIT": "Aufgabe bearbeiten", + "PLACEHOLDER_CARD_TITLE": "Das könnte ein Task sein", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", "TABLE": { "COLUMN": "User-Story", "TITLE_ACTION_FOLD": "Spalte einklappen", @@ -1073,16 +1155,17 @@ "SEVERITY": "Gewichtung", "PRIORITY": "Priorität", "SUBJECT": "Thema", + "VOTES": "Stimmen", "STATUS": "Status", "CREATED": "Erstellt", "ASSIGNED_TO": "Zugeordnet" }, "TITLE_ACTION_CHANGE_STATUS": "Status wechseln", "TITLE_ACTION_ASSIGNED_TO": "Zugeordnet zu ", + "BLOCKED": "Blockiert", "EMPTY": { "TITLE": "Es gibt keine Probleme zu berichten :-)", - "SUBTITLE": "Haben Sie ein Problem gefunden?", - "ACTION_CREATE_ISSUE": "Eine neues Ticket anlegen" + "SUBTITLE": "Haben Sie ein Problem gefunden?" } } }, @@ -1104,7 +1187,9 @@ "ACTION_HIDE_ARCHIVED": "Archivierte verbergen", "HIDDEN_USER_STORIES": "Die User-Stories in diesem Status werden automatisch verborgen ", "ARCHIVED": "Sie haben es archiviert", - "UNDO_ARCHIVED": "Wiederholen Sie Drag & Drop zum Rückgängigmachen " + "UNDO_ARCHIVED": "Wiederholen Sie Drag & Drop zum Rückgängigmachen ", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories können Unteraufgaben für weitere Anforderungen haben" }, "SEARCH": { "PAGE_TITLE": "Suche - {{projectName}}", @@ -1182,7 +1267,9 @@ "BIO": "Bio (max. 210 Zeichen)", "PLACEHOLDER_BIO": "Erzählen Sie etwas über sich", "LANGUAGE": "Sprache", - "LANGUAGE_DEFAULT": "-- benutzen Sie eine vorgegebene Sprache --" + "LANGUAGE_DEFAULT": "-- benutzen Sie eine vorgegebene Sprache --", + "THEME": "Theme", + "THEME_DEFAULT": "-- Standard-Theme benutzen --" } }, "WIZARD": { @@ -1234,18 +1321,91 @@ "NEW_PROJECT": "{{username}} erstellte das Projekt {{project_name}}", "MILESTONE_UPDATED": "{{username}} aktualisierte den Sprint {{obj_name}}", "US_UPDATED": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der User-Story {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der User-Story {{obj_name}} zu {{new_value}}", + "US_UPDATED_POINTS": "{{username}} has updated '{{role_name}}' points of the US {{obj_name}} to {{new_value}}", "ISSUE_UPDATED": "{{username}} aktualisierte das Attribut \"{{field_name}}\" des Tickets {{obj_name}}", - "TASK_UPDATED": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der Aufgabe {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} aktualisierte das Attribut \"{{field_name}}\" des Tickets {{obj_name}} zu {{new_value}}", + "TASK_UPDATED": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der Aufgabe {{obj_name}} zu {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der Aufgabe {{obj_name}} zu {{new_value}}", "TASK_UPDATED_WITH_US": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der Aufgabe {{obj_name}} von User-Story {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} aktualisierte das Attribut \"{{field_name}}\" der Aufgabe {{obj_name}} die zu der User-Story gehört {{us_name}} zu {{new_value}}", "WIKI_UPDATED": "{{username}} aktualisierte die WIKI Seite {{obj_name}}", "NEW_COMMENT_US": "{{username}} schrieb einen Kommentar in der User-Story {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} schrieb einen Kommentar im Ticket {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} schrieb einen Kommentar in der Aufgabe {{obj_name}}", "NEW_MEMBER": "{{project_name}} hat ein neues Mitglied", "US_ADDED_MILESTONE": "{{username}} fügte dem Sprint {{sprint_name}} die User-Story {{obj_name}} hinzu", + "US_MOVED": "{{username}} has moved the US {{obj_name}}", "US_REMOVED_FROM_MILESTONE": "{{username}} fügte der Auftragsliste die User-Story {{obj_name}} hinzu", "BLOCKED": "{{username}} vermerkte die Blockierung von {{obj_name}}", "UNBLOCKED": "{{username}} hob die Blockierung von {{obj_name}} auf", "NEW_USER": "{{username}} ist Taiga beigetreten" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "An external app requires authentication", + "PAGE_DESCRIPTION": "An external app requires authentication", + "AUTHORIZATION_REQUEST": "{{application}} erlauben ihren Taiga Account zu benutzen?", + "LOGIN_WITH_ANOTHER_USER": "Mit einem anderen Benutzer einloggen.", + "AUTHORIZE_APP": "App autorisieren", + "CANCEL": "Abbrechen" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Ihr Projekt", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Zuletzt bearbeitet", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Beobachtet", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "Du arbeitest schon mit Taiga ;)" + }, + "STEP4": { + "TITLE": "Los geht’s", + "TEXT1": "Du kannst anfangen, indem du dein erstes Taiga-Projekt startest", + "TEXT2": "Viel Glück!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Projektzusammenfassung", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Produkt Backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "User-Stories", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Personalisiere deinen Workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User-Stories und Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "User-Stories hinzufügen", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Viel Glück!" + } + } } } \ No newline at end of file diff --git a/app/locales/locale-en.json b/app/locales/locale-en.json index eb431b4d..c959212b 100644 --- a/app/locales/locale-en.json +++ b/app/locales/locale-en.json @@ -36,6 +36,7 @@ "EXTERNAL_USER": "an external user", "GENERIC_ERROR": "One of our Oompa Loompas says {{error}}.", "IOCAINE_TEXT": "Feeling a bit overwhelmed by a task? Make sure others know about it by clicking on Iocaine when editing a task. It's possible to become immune to this (fictional) deadly poison by consuming small amounts over time just as it's possible to get better at what you do by occasionally taking on extra challenges!", + "CAPSLOCK_WARNING": "Be careful! You're writing with capital letters and this input is case sensitive.", "FORM_ERRORS": { "DEFAULT_MESSAGE": "This value seems to be invalid.", "TYPE_EMAIL": "This value should be a valid email.", @@ -130,7 +131,8 @@ "POINTS": "Points", "BLOCKED_NOTE": "blocked note", "IS_BLOCKED": "is blocked", - "REF": "Ref" + "REF": "Ref", + "VOTES": "Votes" }, "ROLES": { "ALL": "All" @@ -148,11 +150,27 @@ "OPEN": "Open" }, "WATCHERS": { - "ADD": "Add watcher", - "TITLE": "watchers", + "ADD": "Add watchers", + "TITLE_ADD": "Add a project member to the watchers list", "DELETE": "Delete watcher", "TITLE_LIGHTBOX_DELETE_WARTCHER": "Delete watcher..." }, + "WATCH_BUTTON": { + "WATCH": "Watch", + "WATCHING": "Watching", + "UNWATCH": "Unwatch", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Watch/Unwatch this item", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Upvoted", + "DOWNVOTE": "Downvote", + "VOTERS": "Voters", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "Custom Fields", "SAVE": "Save Custom Field", @@ -294,7 +312,7 @@ }, "CHANGE_PASSWORD": { "PAGE_TITLE": "Change you password - Taiga", - "PAGE_DESCRIPTION": "Set a new passoword for your Taiga account and hey!, you may want to eat some more iron-rich food, it's good for your brain :P", + "PAGE_DESCRIPTION": "Set a new password for your Taiga account and hey!, you may want to eat some more iron-rich food, it's good for your brain :P", "SECTION_NAME": "Change password", "FIELD_CURRENT_PASSWORD": "Current password", "PLACEHOLDER_CURRENT_PASSWORD": "Your current password (or empty if you have no password yet)", @@ -307,12 +325,10 @@ "CHANGE_PASSWORD_RECOVERY_FORM": { "TITLE": "Create a new Taiga pass", "SUBTITLE": "And hey, you may want to eat some more iron-rich food, it's good for your brain :P", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Recover password token", - "LINK_NEED_TOKEN": "Need one?", - "TITLE_LINK_NEED_TOKEN": "Did you need a token to recover your password because you forgot it?", "PLACEHOLDER_NEW_PASSWORD": "New password", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Re-type new password", "ACTION_RESET_PASSWORD": "Reset Password", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", "SUCCESS": "Our Oompa Loompas saved your new password.
Try to sign in with it." }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "Accept the invitation to join a project in Taiga, a project management platform for startups and agile developers & designers who want a simple, beautiful tool that makes work truly enjoyable." }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Ooops, we have a problem
Our Oompa Loompas can't find your invitation.", + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", "SUCCESS": "You've successfully joined this project, Welcome to {{project_name}}", "ERROR": "According to our Oompa Loompas, your are not registered yet or typed an invalid password." }, "HOME": { "PAGE_TITLE": "Home - Taiga", "PAGE_DESCRIPTION": "The Taiga home page with your main projects and all your assigned and watched user stories, tasks and issues", - "EMPTY_WATCHING": "Follow the projects, User Stories, Tasks, Issues... that you want to know about :)", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", "EMPTY_PROJECT_LIST": "You don't have any projects yet", "WORKING_ON_SECTION": "Working on", "WATCHING_SECTION": "Watching" @@ -343,7 +360,7 @@ "DESCRIPTION": "Type a short description", "DEPRECATED": "(deprecated)", "DEPRECATED_FILE": "Deprecated?", - "ADD": "Add new attachment. <%- maxFileSizeMsg %>", + "ADD": "Add new attachment. {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Max. size: {{maxFileSize}}]", "SHOW_DEPRECATED": "+ show deprecated atachments", "HIDE_DEPRECATED": "- hide deprecated atachments", @@ -407,15 +424,20 @@ "MEETUP": "Meet Up", "MEETUP_DESCRIPTION": "Choose your videoconference system. Even developers need face to face contact.", "SELECT_VIDEOCONFERENCE": "Select a videoconference system", - "SALT_CHAT_ROOM": "If you want you can append a salt code to the name of the chat room" + "SALT_CHAT_ROOM": "If you want you can append a salt code to the name of the chat room", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Custom", + "URL_CHAT_ROOM": "URL of your chat room" }, "PROJECT_PROFILE": { "PAGE_TITLE": "{{sectionName}} - Project profile - {{projectName}}", "PROJECT_DETAILS": "Project details", "PROJECT_NAME": "Project name", "PROJECT_SLUG": "Project slug", - "NUMBER_SPRINTS": "Number of sprints", - "NUMBER_US_POINTS": "Number of US points", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", "TAGS": "Tags", "DESCRIPTION": "Description", "PUBLIC_PROJECT": "Public project", @@ -448,7 +470,10 @@ "TASK_DESCRIPTION": "Tasks custom fields", "TASK_ADD": "Add a custom field in tasks", "ISSUE_DESCRIPTION": "Issues custom fields", - "ISSUE_ADD": "Add a custom field in issues" + "ISSUE_ADD": "Add a custom field in issues", + "FIELD_TYPE_TEXT": "Text", + "FIELD_TYPE_MULTI": "Multi-line", + "FIELD_TYPE_DATE": "Date" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - Project values - {{projectName}}", @@ -490,6 +515,9 @@ "PAGE_TITLE": "Roles - {{projectName}}", "WARNING_NO_ROLE": "Be careful, no role in your project will be able to estimate the point value for user stories", "HELP_ROLE_ENABLED": "When enabled, members assigned to this role will be able to estimate the point value for user stories", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", "COUNT_MEMBERS": "{{ role.members_count }} members with this role", "TITLE_DELETE_ROLE": "Delete Role", "REPLACEMENT_ROLE": "All the users with this role will be moved to", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "Active", "STATUS_PENDING": "Pending", "DELETE_MEMBER": "Delete member", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "We've sent the invitation again to '{{email}}'.", "ERROR_SEND_INVITATION": "We haven't sent the invitation.", "SUCCESS_DELETE": "We've deleted {{message}}.", @@ -626,23 +655,47 @@ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", "EDIT": "Edit profile", "FOLLOW": "Follow", - "PROJECTS": "Projects", "CLOSED_US": "Closed US", + "PROJECTS": "Projects", + "PROJECTS_EMPTY": "{{username}} doesn't' have projects yet", "CONTACTS": "Contacts", - "REPORT": "Report Abuse", - "ACTIVITY_TAB": "Activity Tab", - "PROJECTS_TAB": "Projects Tab", - "CONTACTS_TAB": "Contacts Tab", - "FAVORITES_TAB": "Favorites Tab", "CONTACTS_EMPTY": "{{username}} doesn't have contacts yet", "CURRENT_USER_CONTACTS_EMPTY": "You don't have contacts yet", "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "The people with whom you work at Taiga will be your contacts automatically", - "PROJECTS_EMPTY": "{{username}} doesn't' have projects yet" + "REPORT": "Report Abuse", + "TABS": { + "ACTIVITY_TAB": "Activity", + "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "PROJECTS_TAB": "Projects", + "PROJECTS_TAB_TITLE": "List all projects that this user is a member", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Votes", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Watched", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Contacts", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } }, "PROFILE_SIDEBAR": { "TITLE": "Your profile", "DESCRIPTION": "People can see everything you do and what are you working on. Add a nice bio to give an enhanced version of your information.", - "ADD_INFO": "Edit your bio" + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Type something...", + "FILTER_TYPE_ALL": "All", + "FILTER_TYPE_ALL_TITLE": "Show all", + "FILTER_TYPE_PROJECTS": "Projects", + "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", + "FILTER_TYPE_TASKS": "Tasks", + "FILTER_TYPE_TASK_TITLES": "Show only tasks", + "FILTER_TYPE_ISSUES": "Issues", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." } }, "PROJECT": { @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "Our Oompa Loompas have some problems importing your dump data: {{error_message}}", "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) is too heavy for our Oompa Loompas, try it with a smaller than ({{maxFileSize}})", "SYNC_SUCCESS": "Your project has been imported successfuly" + }, + "LIKE_BUTTON": { + "LIKE": "Like", + "LIKED": "Liked", + "UNLIKE": "Unlike", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Watch", + "WATCHING": "Watching", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Receive all notifications", + "NOTIFY_ALL_TITLE": "Receive all notifications for this project", + "NOTIFY_INVOLVED": "Only involved", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Unwatch", + "UNWATCH_TITLE": "Unwatch this project" + } } }, "LIGHTBOX": { @@ -785,7 +859,7 @@ "SECTION_NAME": "User story details", "LINK_TASKBOARD": "Taskboard", "TITLE_LINK_TASKBOARD": "Go to the taskboard", - "TOTAL_POINTS": "total", + "TOTAL_POINTS": "total points", "ADD": "+ Add a new User Story", "ADD_BULK": "Add some new User Stories in bulk", "PROMOTED": "This US has been promoted from Issue:", @@ -872,6 +946,10 @@ "PAGE_TITLE": "Backlog - {{projectName}}", "PAGE_DESCRIPTION": "The backlog panel, with user stories and sprints of the project {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Backlog", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Admin", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "Move to Current Sprint", "SHOW_FILTERS": "Show filters", "SHOW_TAGS": "Show tags", @@ -892,10 +970,10 @@ "CHART": { "XAXIS_LABEL": "Sprints", "YAXIS_LABEL": "Points", - "OPTIMAL": "Optimal pending points for sprint {{xval}} should be {{yval}}", - "REAL": "Real pending points for sprint {{xval}} is {{yval}}", - "INCREMENT_TEAM": "Incremented points by team requirements for sprint {{xval}} is {{yval}}", - "INCREMENT_CLIENT": "Incremented points by client requirements for sprint {{xval}} is {{yval}}" + "OPTIMAL": "Optimal pending points for sprint \"{{sprintName}}\" should be {{value}}", + "REAL": "Real pending points for sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_TEAM": "Incremented points by team requirements for sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_CLIENT": "Incremented points by client requirements for sprint \"{{sprintName}}\" is {{value}}" }, "TAGS": { "TOGGLE": "Toggle tags visibility", @@ -912,7 +990,8 @@ "OPEN_TASKS": "open
tasks", "CLOSED_TASKS": "closed
tasks", "IOCAINE_DOSES": "iocaine
doses", - "SHOW_STATISTICS_TITLE": "Show statistics" + "SHOW_STATISTICS_TITLE": "Show statistics", + "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph" }, "SUMMARY": { "PROJECT_POINTS": "project
points", @@ -935,19 +1014,20 @@ "LINK_TASKBOARD": "Sprint Taskboard", "TITLE_LINK_TASKBOARD": "Go to Taskboard of \"{{name}}\"", "NUMBER_SPRINTS": "
sprints", - "TITLE_ACTION_NEW_SPRINT": "+ New sprint", - "ACTION_NEW_SPRINT": "+ New sprint", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", "ACTION_SHOW_CLOSED_SPRINTS": "Show closed sprints", "ACTION_HIDE_CLOSED_SPRINTS": "Hide closed sprints" } }, "ERROR": { "TEXT1": "Something happened and our Oompa Loompas are working on it.", - "TEXT2": "Try reloading again soon.", "NOT_FOUND": "Not found", "NOT_FOUND_TEXT": "Error 404. The page you are looking for no longer exists. Perhaps you can return back to TAIGA homepage and see if you can find what you are looking for.", "PERMISSION_DENIED": "Permission denied", - "PERMISSION_DENIED_CODE": "Error 403.", + "PERMISSION_DENIED_TEXT": "You don't have permission to access to this page.", "VERSION_ERROR": "Someone inside Taiga has changed this before and our Oompa Loompas cannot apply your changes. Please reload and apply your changes again (they will be lost)." }, "TASKBOARD": { @@ -958,6 +1038,8 @@ "TITLE_ACTION_ADD_BULK": "Add some new Tasks in bulk", "TITLE_ACTION_ASSIGN": "Assign task", "TITLE_ACTION_EDIT": "Edit task", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", "TABLE": { "COLUMN": "User story", "TITLE_ACTION_FOLD": "Fold column", @@ -1076,16 +1158,17 @@ "SEVERITY": "Severity", "PRIORITY": "Priority", "SUBJECT": "Subject", + "VOTES": "Votes", "STATUS": "Status", "CREATED": "Created", "ASSIGNED_TO": "Assigned to" }, "TITLE_ACTION_CHANGE_STATUS": "Change status", "TITLE_ACTION_ASSIGNED_TO": "Assigned to", + "BLOCKED": "Blocked", "EMPTY": { "TITLE": "There are no issues to report :-)", - "SUBTITLE": "Did you find an issue?", - "ACTION_CREATE_ISSUE": "Create a new Issue" + "SUBTITLE": "Did you find an issue?" } } }, @@ -1107,7 +1190,9 @@ "ACTION_HIDE_ARCHIVED": "Hide archived", "HIDDEN_USER_STORIES": "The user stories in this status are hidden by default", "ARCHIVED": "You have archived", - "UNDO_ARCHIVED": "Drag & drop again to undo" + "UNDO_ARCHIVED": "Drag & drop again to undo", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" }, "SEARCH": { "PAGE_TITLE": "Search - {{projectName}}", @@ -1185,7 +1270,9 @@ "BIO": "Bio (max. 210 chars)", "PLACEHOLDER_BIO": "Tell us something about you", "LANGUAGE": "Language", - "LANGUAGE_DEFAULT": "-- use default language --" + "LANGUAGE_DEFAULT": "-- use default language --", + "THEME": "Theme", + "THEME_DEFAULT": "-- use default theme --" } }, "WIZARD": { @@ -1237,18 +1324,91 @@ "NEW_PROJECT": "{{username}} created the project {{project_name}}", "MILESTONE_UPDATED": "{{username}} has updated the sprint {{obj_name}}", "US_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}} to {{new_value}}", + "US_UPDATED_POINTS": "{{username}} has updated '{{role_name}}' points of the US {{obj_name}} to {{new_value}}", "ISSUE_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the issue {{obj_name}}", - "TASK_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the issue {{obj_name}} to {{new_value}}", + "TASK_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} to {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} to {{new_value}}", "TASK_UPDATED_WITH_US": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}} to {{new_value}}", "WIKI_UPDATED": "{{username}} has updated the wiki page {{obj_name}}", "NEW_COMMENT_US": "{{username}} has commented in the US {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} has commented in the issue {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} has commented in the task {{obj_name}}", "NEW_MEMBER": "{{project_name}} has a new member", "US_ADDED_MILESTONE": "{{username}} has added the US {{obj_name}} to {{sprint_name}}", + "US_MOVED": "{{username}} has moved the US {{obj_name}}", "US_REMOVED_FROM_MILESTONE": "{{username}} has added the US {{obj_name}} to the backlog", "BLOCKED": "{{username}} has blocked {{obj_name}}", "UNBLOCKED": "{{username}} has unblocked {{obj_name}}", "NEW_USER": "{{username}} has joined Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "An external app requires authentication", + "PAGE_DESCRIPTION": "An external app requires authentication", + "AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?", + "LOGIN_WITH_ANOTHER_USER": "Login with another user", + "AUTHORIZE_APP": "Authorize app", + "CANCEL": "Cancel" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Your project", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Working on", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Watching", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "User Stories", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } } } diff --git a/app/locales/locale-es.json b/app/locales/locale-es.json index 3c2f0211..f19c8c35 100644 --- a/app/locales/locale-es.json +++ b/app/locales/locale-es.json @@ -36,6 +36,7 @@ "EXTERNAL_USER": "un usuario externo", "GENERIC_ERROR": "Uno de nuestros Oompa Loompas dice {{error}}.", "IOCAINE_TEXT": "¿Te sientes fuera de tu zona de confort en una tarea? Asegúrate de que los demás están al tanto de ello, marca el check de la Iocaína al editar una tarea. Igual eu era posible llegar a ser inmune a este veneno mortal a base de consumir pequeñas dosis a lo largo del tiempo, es posible conseguir mejor en lo que estás haciendo si afrontas de vez en cuando esta clase de retos!", + "CAPSLOCK_WARNING": "¡Ten cuidado! Usted está escribiendo con mayúsculas y esta entrada es sensible a mayúsculas.", "FORM_ERRORS": { "DEFAULT_MESSAGE": "Este valor parece inválido.", "TYPE_EMAIL": "El valor debe ser un email.", @@ -130,7 +131,8 @@ "POINTS": "Puntos", "BLOCKED_NOTE": "Motivo del bloqueo", "IS_BLOCKED": "está bloqueada", - "REF": "Ref" + "REF": "Ref", + "VOTES": "Votos" }, "ROLES": { "ALL": "Todos" @@ -148,11 +150,27 @@ "OPEN": "Abierto" }, "WATCHERS": { - "ADD": "Añadir un observador...", - "TITLE": "observadores", + "ADD": "Añadir observadores", + "TITLE_ADD": "Añade un miembro del proyecto a la lista de observadores", "DELETE": "Eliminar el observador", "TITLE_LIGHTBOX_DELETE_WARTCHER": "Eliminar el observador..." }, + "WATCH_BUTTON": { + "WATCH": "Observar", + "WATCHING": "Observando", + "UNWATCH": "No observar", + "WATCHERS": "Observadores", + "BUTTON_TITLE": "Observar o no observar este elemento", + "COUNTER_TITLE": "{total, plural, one{un observador} other{# observadores}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Votar", + "UPVOTED": "Votado", + "DOWNVOTE": "No votar", + "VOTERS": "Votos", + "BUTTON_TITLE": "Votar o no este elemento", + "COUNTER_TITLE": "{total, plural, one{un voto} other{# votos}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "Atributo Personalizados", "SAVE": "Guardar atributo personalizado", @@ -307,12 +325,10 @@ "CHANGE_PASSWORD_RECOVERY_FORM": { "TITLE": "Crear una nueva contraseña de Taiga", "SUBTITLE": "Y bueno, es posible que necesites comer más alimentos ricos en hierro, son buenos para tu cerebro :P", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "token de recuperación de contraseña", - "LINK_NEED_TOKEN": "¿Necesitas una?", - "TITLE_LINK_NEED_TOKEN": "¿Necesitas un token para recuperar tu contraseña porque la has olvidado?", "PLACEHOLDER_NEW_PASSWORD": "Nueva contraseña", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Reescriba la nueva contraseña", "ACTION_RESET_PASSWORD": "Restablecer contraseña", + "ERROR": "Nuestros Oompa Loompas no encuentran tu petición de recuperación de contraseña. Prueba a pedirla de nuevo.", "SUCCESS": "Nuestro Oompa Loopas guardaron tu nueva contraseña.
Intenta registrarte con ella" }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "Acepta la invitación para unirte a un proyecto en Taiga, una plataforma de gestión de proyectos orientada a startups y equipos ágiles que buscan una herramienta sencilla, elegante y que les haga disfrutar trabajando." }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Ooops, tenemos un problema
Nuestros Oompa Loompas no pueden encontrar tu invitación.", + "NOT_FOUND": "Nuestros Oompa Loompas no pueden encontrar su invitación.", "SUCCESS": "¡Acabas de unirte al proyecto! Bienvenido a {{project_name}}", "ERROR": "Según nuestros Oompa Loompas tú no estás registrado o has escrito mal tu contraseña." }, "HOME": { "PAGE_TITLE": "Inicio - Taiga", "PAGE_DESCRIPTION": "Página de inicio de Taiga, con tus proyectos principales y tus historias de usuario, tareas y peticiones en progreso asignadas y las que observas.", - "EMPTY_WATCHING": "Sigue aquellos proyectos, historias de usuario, tareas, peticiones... sobre los que quieras estar al tanto :)", + "EMPTY_WORKING_ON": "Parece vacío no? Empiece a trabajar con Taiga y verá aquí las historias, las tareas y los incidentes en los que está trabajando.", + "EMPTY_WATCHING": "Sigue Historias de Usuario, tareas, peticiones... sobre las que quieres estar informado :)", "EMPTY_PROJECT_LIST": "Todavía no tienes ningún proyecto", "WORKING_ON_SECTION": "Trabajando en", "WATCHING_SECTION": "Observando" @@ -343,7 +360,7 @@ "DESCRIPTION": "Escribe una pequeña descripción", "DEPRECATED": "(obsoleto)", "DEPRECATED_FILE": "¿Desactualizado?", - "ADD": "Agrega nuevos adjunto", + "ADD": "Agrega nuevos adjunto. {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Tamaño Max. : {{maxFileSize}}]", "SHOW_DEPRECATED": "+ muestra adjuntos desactualizados", "HIDE_DEPRECATED": "- ocultar adjuntos obsoletos", @@ -407,15 +424,20 @@ "MEETUP": "Meet Up", "MEETUP_DESCRIPTION": "Elige tu sistema de videoconferencia. Incluso los desarrolladores necesitan el contacto cara a cara.", "SELECT_VIDEOCONFERENCE": "Elige un sistema de videoconferencia", - "SALT_CHAT_ROOM": "Puedes añadirle un código salt al nombre del chat room" + "SALT_CHAT_ROOM": "Puedes añadirle un código salt al nombre del chat room", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Personalizado", + "URL_CHAT_ROOM": "URL de tu chat room" }, "PROJECT_PROFILE": { "PAGE_TITLE": "{{sectionName}} - Perfil de Proyecto - {{projectName}}\n", "PROJECT_DETAILS": "Info del proyecto", "PROJECT_NAME": "Nombre del proyecto", "PROJECT_SLUG": "Slug de proyecto", - "NUMBER_SPRINTS": "Número de sprints totales estimado", - "NUMBER_US_POINTS": "Número de puntos de historia totales estimados", + "NUMBER_SPRINTS": "Número de sprints (0 para cantidad indeterminada)", + "NUMBER_US_POINTS": "Número de puntos de historias de usuario (0 para indeterminado)", "TAGS": "Etiquetas", "DESCRIPTION": "Descripción", "PUBLIC_PROJECT": "Proyecto público", @@ -448,7 +470,10 @@ "TASK_DESCRIPTION": "Atributos personalizados de tareas", "TASK_ADD": "Añadir un atributo personalizado en las tareas", "ISSUE_DESCRIPTION": "Atributos personalizados de peticiones", - "ISSUE_ADD": "Añadir un atributo personalizado en las peticiones" + "ISSUE_ADD": "Añadir un atributo personalizado en las peticiones", + "FIELD_TYPE_TEXT": "Texto", + "FIELD_TYPE_MULTI": "Multilínea", + "FIELD_TYPE_DATE": "Fecha" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - Valores del Proyectos - {{projectName}}", @@ -490,6 +515,9 @@ "PAGE_TITLE": "Roles - {{projectName}}", "WARNING_NO_ROLE": "Ojo, ningún rol en tu proyecto podrá estimar historias de usuario", "HELP_ROLE_ENABLED": "Si lo activas, los miembros que posean este rol serán capaces de estimar las histórias de usuario", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Deshabilitar las estimaciones para este rol", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "Si desactivas los permisos de estimación para el rol {{roleName}} todas las estimaciones previas hechas por ese rol se eliminarán", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "¿Seguro de que deseas deshabilitar las estimaciones para este rol?", "COUNT_MEMBERS": "{{ role.members_count }} miembros con este rol", "TITLE_DELETE_ROLE": "Borrar Rol", "REPLACEMENT_ROLE": "Todos los usuarios con este rol serán movidos a", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "Activado", "STATUS_PENDING": "Pendiente", "DELETE_MEMBER": "Borrar miembro", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "Hemos enviado nuevamente la invitación a '{{email}}'.", "ERROR_SEND_INVITATION": "No hemos enviado la invitación.", "SUCCESS_DELETE": "Hemos eliminado {{message}}.", @@ -626,23 +655,47 @@ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", "EDIT": "Editar perfil", "FOLLOW": "Seguir", - "PROJECTS": "Proyectos", "CLOSED_US": "Historias cerradas", + "PROJECTS": "Proyectos", + "PROJECTS_EMPTY": "{{username}} no tiene proyectos todavía", "CONTACTS": "Contactos", - "REPORT": "Reportar Abuso", - "ACTIVITY_TAB": "Pestaña de Actividad", - "PROJECTS_TAB": "Pestaña de Proyectos", - "CONTACTS_TAB": "Pestaña de Contactos", - "FAVORITES_TAB": "Pestaña de Favoritos", "CONTACTS_EMPTY": "{{username}} no tiene contactos todavía", "CURRENT_USER_CONTACTS_EMPTY": "No tienes contactos todavía", "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Las personas con las que trabajas en Taiga serán tus contactos automáticamente", - "PROJECTS_EMPTY": "{{username}} no tiene proyectos todavía" + "REPORT": "Reportar Abuso", + "TABS": { + "ACTIVITY_TAB": "Actividad", + "ACTIVITY_TAB_TITLE": "Muestra toda la actividad de este usuario", + "PROJECTS_TAB": "Proyectos", + "PROJECTS_TAB_TITLE": "Lista todo los proyectos en los que este usuario es miembro", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "Lista todo lo que le gusta a este usuario", + "VOTES_TAB": "Votos", + "VOTES_TAB_TITLE": "Lista todos los votos de este usuario", + "WATCHED_TAB": "Observado", + "WATCHED_TAB_TITLE": "Lista todos los elementos observados por este usuario", + "CONTACTS_TAB": "Contactos", + "CONTACTS_TAB_TITLE": "Lista todos los contactos hechos por este usuario" + } }, "PROFILE_SIDEBAR": { "TITLE": "Tu perfil", "DESCRIPTION": "La gente puede ver aquello que haces y en qué estás trabajando. Añade una buena bio para que puedan ver la mejor versión de tu perfil.", - "ADD_INFO": "Edita tu bio" + "ADD_INFO": "Editar bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Escribe algo...", + "FILTER_TYPE_ALL": "Todas", + "FILTER_TYPE_ALL_TITLE": "Mostrar todos", + "FILTER_TYPE_PROJECTS": "Proyectos", + "FILTER_TYPE_PROJECT_TITLES": "Mostrar sólo proyectos", + "FILTER_TYPE_USER_STORIES": "Historias", + "FILTER_TYPE_USER_STORIES_TITLES": "Mostrar sólo historias de usuario", + "FILTER_TYPE_TASKS": "Tareas", + "FILTER_TYPE_TASK_TITLES": "Mostrar sólo tareas", + "FILTER_TYPE_ISSUES": "Peticiones", + "FILTER_TYPE_ISSUES_TITLE": "Mostrar sólo peticiones", + "EMPTY_TITLE": "Parece que no se ha encontrado nada." } }, "PROJECT": { @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "Nuestros Oompa Loompas tienen algunos problemas importando tus datos: {{error_message}}", "ERROR_MAX_SIZE_EXCEEDED": "El fichero '{{fileName}}' ({{fileSize}}) es demasiado pesado para nuestros Oompa Loompas, prueba con uno de menos de ({{maxFileSize}}).", "SYNC_SUCCESS": "Tu proyecto se ha importado con éxito." + }, + "LIKE_BUTTON": { + "LIKE": "Me gusta", + "LIKED": "Me gusta", + "UNLIKE": "Ya no!", + "BUTTON_TITLE": "Marca o desmarcar como favorito este proyecto", + "COUNTER_TITLE": "{total, plural, one{un fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Observa este proyecto y define la política de notificación", + "WATCH": "Observar", + "WATCHING": "Observando", + "COUNTER_TITLE": "{total, plural, one{un observador} other{# observadores}}", + "OPTIONS": { + "NOTIFY_ALL": "Recibir todas las notificaciones", + "NOTIFY_ALL_TITLE": "Recibir todas las notificaciones de este proyecto", + "NOTIFY_INVOLVED": "Estoy involucrado", + "NOTIFY_INVOLVED_TITLE": "Recibe notificaciones sólo cuando tú estéß involucrado", + "UNWATCH": "No observar", + "UNWATCH_TITLE": "Dejar de observar este proyecto" + } } }, "LIGHTBOX": { @@ -783,7 +857,7 @@ "SECTION_NAME": "Detalles de historia de usuario", "LINK_TASKBOARD": "Panel de tareas", "TITLE_LINK_TASKBOARD": "Ir al panel de tareas", - "TOTAL_POINTS": "total", + "TOTAL_POINTS": "puntos totales", "ADD": "+ Añadir una Historia de Usuario", "ADD_BULK": "Añadir nuevas Historias de Usuario en bloque", "PROMOTED": "Esta historia ha sido promovida desde la petición:", @@ -870,6 +944,10 @@ "PAGE_TITLE": "Backlog - {{projectName}}", "PAGE_DESCRIPTION": "El panel del backlog, con las historias de usuario y sprints del proyecto {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Backlog", + "CUSTOMIZE_GRAPH": "Customiza la gráfica de tu backlog", + "CUSTOMIZE_GRAPH_TEXT": "Para tener una gráfica bonita que te ayude a seguir la evolución del proyecto tienes que fijar los puntos y sprints en la", + "CUSTOMIZE_GRAPH_ADMIN": "Admin", + "CUSTOMIZE_GRAPH_TITLE": "Modifica los puntos y sprints a través de la zona de Administración", "MOVE_US_TO_CURRENT_SPRINT": "Mover al Sprint en curso", "SHOW_FILTERS": "Mostrar filtros", "SHOW_TAGS": "Ver etiquetas", @@ -889,10 +967,10 @@ "CHART": { "XAXIS_LABEL": "Sprints", "YAXIS_LABEL": "Puntos", - "OPTIMAL": "El número de puntos óptimos pendientes para el sprint {{xval}} debería ser de {{yval}}", - "REAL": "El número real de puntos pendientes para el sprint {{xval}} es de {{yval}}", - "INCREMENT_TEAM": "El número de puntos incrementados por requerimientos del equipo para el sprint {{xval}} es de {{yval}}", - "INCREMENT_CLIENT": "El número de puntos incrementados por requerimientos del cliente para el sprint {{xval}} es de {{yval}}" + "OPTIMAL": "El número de puntos óptimos pendientes para el sprint \"{{sprintName}}\" debería ser de {{value}}", + "REAL": "El número real de puntos pendientes para el sprint \"{{sprintName}}\" es de {{value}}", + "INCREMENT_TEAM": "El número de puntos incrementados por requerimientos del equipo para el sprint \"{{sprintName}}\" es de {{value}}", + "INCREMENT_CLIENT": "El número de puntos incrementados por requerimientos del cliente para el sprint \"{{sprintName}}\" es de {{value}}" }, "TAGS": { "TOGGLE": "Cambia la visibilidad de los tags", @@ -909,7 +987,8 @@ "OPEN_TASKS": "tareas
abiertas", "CLOSED_TASKS": "tareas
cerradas", "IOCAINE_DOSES": "dosis de
iocaína", - "SHOW_STATISTICS_TITLE": "Ver estadísticas" + "SHOW_STATISTICS_TITLE": "Ver estadísticas", + "TOGGLE_BAKLOG_GRAPH": "Ver/Ocultar gráfica de burndown" }, "SUMMARY": { "PROJECT_POINTS": "puntos
proyecto", @@ -932,19 +1011,20 @@ "LINK_TASKBOARD": "Panel de Tareas del Sprint", "TITLE_LINK_TASKBOARD": "Ir al panel de tareas de \"{{name}}\"", "NUMBER_SPRINTS": "
sprints", - "TITLE_ACTION_NEW_SPRINT": "+ Nuevo Sprint", - "ACTION_NEW_SPRINT": "+ Nuevo sprint", + "EMPTY": "NO TIENES NINGÚN SPRINT", + "WARNING_EMPTY_SPRINT": "Arrastra aquí las historias desde tu backlog para comenzar un nuevo sprint", + "TITLE_ACTION_NEW_SPRINT": "+ Nuevo sprint", + "TEXT_ACTION_NEW_SPRINT": "Es posible que desees crear un nuevo sprint para tu proyecto", "ACTION_SHOW_CLOSED_SPRINTS": "Mostrar sprints cerrados", "ACTION_HIDE_CLOSED_SPRINTS": "Ocultar sprints cerrados" } }, "ERROR": { "TEXT1": "Algo no va bien y nuestros Oompa Loompas están trabajando para resolverlo.", - "TEXT2": "Inténtalo de nuevo más tarde.", "NOT_FOUND": "No encontrado", "NOT_FOUND_TEXT": "Error 404. La página que estás buscando ya no existe. Puedes volver a la página de inicio de TAIGA y ver si encuentras lo que estás buscando. ", "PERMISSION_DENIED": "Permiso denegado", - "PERMISSION_DENIED_CODE": "Error 403.", + "PERMISSION_DENIED_TEXT": "No tienes permisos para acceder a esta página.", "VERSION_ERROR": "Algún compañero se te ha adelantado y ha actualizado esto, nuestros Oompa Loompas no pueden aplicar tus cambios. Por favor, recarga la página y aplícalos nuevamente (se perderán)." }, "TASKBOARD": { @@ -955,6 +1035,8 @@ "TITLE_ACTION_ADD_BULK": "Añadir nuevas tareas en bloque", "TITLE_ACTION_ASSIGN": "Asignar tarea", "TITLE_ACTION_EDIT": "Editar tarea", + "PLACEHOLDER_CARD_TITLE": "Esto podría ser una tarea", + "PLACEHOLDER_CARD_TEXT": "Divide las historias en tareas para controlarlas de manera separada", "TABLE": { "COLUMN": "Historia de usuario", "TITLE_ACTION_FOLD": "Plegar columna", @@ -1073,16 +1155,17 @@ "SEVERITY": "Gravedad", "PRIORITY": "Prioridad", "SUBJECT": "Asunto", + "VOTES": "Votos", "STATUS": "Estado", "CREATED": "Creado", "ASSIGNED_TO": "Asignado a" }, "TITLE_ACTION_CHANGE_STATUS": "Cambio de estado", "TITLE_ACTION_ASSIGNED_TO": "Asignado a", + "BLOCKED": "Bloqueada", "EMPTY": { "TITLE": "No hay peticiones a reportar :-)", - "SUBTITLE": "¿Ha encontrado una petición?", - "ACTION_CREATE_ISSUE": "Crear una nueva petición" + "SUBTITLE": "¿Ha encontrado una petición?" } } }, @@ -1104,7 +1187,9 @@ "ACTION_HIDE_ARCHIVED": "Ocultar archivados", "HIDDEN_USER_STORIES": "Las historias de usuario en este estado están ocultas por defecto", "ARCHIVED": "Tu has archivado", - "UNDO_ARCHIVED": "Arrástrala y suéltala de nuevo para deshacer el cambio" + "UNDO_ARCHIVED": "Arrástrala y suéltala de nuevo para deshacer el cambio", + "PLACEHOLDER_CARD_TITLE": "Estas son tus Historias de Usuario", + "PLACEHOLDER_CARD_TEXT": "Las historias pueden tener tareas para separar los requerimientos" }, "SEARCH": { "PAGE_TITLE": "Buscar - {{projectName}}", @@ -1182,7 +1267,9 @@ "BIO": "Bio (max. 210 caracteres)", "PLACEHOLDER_BIO": "Dinos algo acerca de ti", "LANGUAGE": "Idioma", - "LANGUAGE_DEFAULT": "- usar idioma por defecto -" + "LANGUAGE_DEFAULT": "- usar idioma por defecto -", + "THEME": "Tema", + "THEME_DEFAULT": "-- usar tema por defecto --" } }, "WIZARD": { @@ -1234,18 +1321,91 @@ "NEW_PROJECT": "{{username}} creó el proyecto {{project_name}}", "MILESTONE_UPDATED": "{{username}} ha actualizado el sprint {{obj_name}}", "US_UPDATED": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la historia {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la historia {{obj_name}} a {{new_value}}", + "US_UPDATED_POINTS": "{{username}} ha actualizado los puntos de '{{role_name}}' de la historia {{obj_name}} a {{new_value}}", "ISSUE_UPDATED": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la petición {{obj_name}}", - "TASK_UPDATED": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la tarea {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la petición {{obj_name}} a {{new_value}}", + "TASK_UPDATED": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la tarea {{obj_name}} a {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la tarea {{obj_name}} a {{new_value}}", "TASK_UPDATED_WITH_US": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la tarea {{obj_name}} que proviene de la historia {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} ha actualizado el atributo \"{{field_name}}\" de la tarea {{obj_name}} que pertenece a la historia {{us_name}} a {{new_value}}", "WIKI_UPDATED": "{{username}} ha actualizado la página del wiki {{obj_name}}", "NEW_COMMENT_US": "{{username}} ha añadido un comentado en la historia {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} ha añadido un comentado en la petición {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} ha añadido un comentado en la tarea {{obj_name}}", "NEW_MEMBER": "{{project_name}} tiene un nuevo miembro", "US_ADDED_MILESTONE": "{{username}} ha añadido la historia {{obj_name}} a {{sprint_name}}", + "US_MOVED": "{{username}} ha movido la historia {{obj_name}}", "US_REMOVED_FROM_MILESTONE": "{{username}} ha añadido la historia {{obj_name}} al backlog", "BLOCKED": "{{username}} ha bloqueado {{obj_name}}", "UNBLOCKED": "{{username}} ha desbloqueado {{obj_name}}", "NEW_USER": "{{username}} se ha unido a Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "Haciendo click en \"Registrarme\"', aceptas nuestros
términos de servicio y nuestra política de privacidad." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "Una aplicación externa requiere autenticación", + "PAGE_DESCRIPTION": "Una aplicación externa requiere autenticación", + "AUTHORIZATION_REQUEST": "¿Autoriza {{aplicación}} para que utilice tu cuenta de Taiga?", + "LOGIN_WITH_ANOTHER_USER": "Ingresar con otro usuario", + "AUTHORIZE_APP": "Autorizar aplicación", + "CANCEL": "Cancelar" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Su proyecto", + "TEXT": "Bienvenido! Aquí encontrarás los proyectos en los que estás participando. Te hemos dejado plantillas con proyectos de ejemplo para ayudarte a descubrir el poder de Taiga." + }, + "STEP2": { + "TITLE": "Trabajando en", + "TEXT": "Aquí encontrarás las Historias de Usuario, Tareas y Peticiones en las que estás trabajando." + }, + "STEP3": { + "TITLE": "Observando", + "TEXT1": "Y justo aquí encontrarás aquellas sobre las que quieres saber algo más", + "TEXT2": "Ya estás trabajando con Taiga ;)" + }, + "STEP4": { + "TITLE": "Comencemos", + "TEXT1": "Puedes comenzar creando tu primer proyecto en Taiga", + "TEXT2": "Buena suerte!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Resumen del proyecto", + "TEXT1": "Aquí verás el estado de tu proyecto.", + "TEXT2": "Puedes cambiar todos los ajustes del proyecto a través de la zona de administración." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "El backlog es la lista de requerimientos (Historias de Usuario) del proyecto. Aquí es donde planificarás tus sprints" + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Los sprints son periodos cortos de tiempo (normalmente de 2 semanas) durante los cuales un trabajo específico tiene que ser completado y entregado" + }, + "STEP4": { + "TITLE": "Historias de Usuario", + "TEXT": "Estos son los requerimientos de alto nivel. Puedes añadirlos al backlog y arrastrarlos al sprint en el que deban ser completados" + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customiza to workflow", + "TEXT": "Fila las columnas que necesites para asociar los estados the tu workflow en la zona de administración." + }, + "STEP2": { + "TITLE": "Historias de usuario y tareas", + "TEXT": "Las historias de usuario son los requerimientos de alto nivel. Puedes arrastrarlos a las diferentes columnas." + }, + "STEP3": { + "TITLE": "Añadir Historias de Usuario", + "TEXT1": "Puede que quieras añadir una sóla Historia de Usuario (add US icon) o un grupo de ellas (bulk icon)", + "TEXT2": "Buena suerte!" + } + } } } \ No newline at end of file diff --git a/app/locales/locale-fi.json b/app/locales/locale-fi.json index 85bf6a4c..214b3685 100644 --- a/app/locales/locale-fi.json +++ b/app/locales/locale-fi.json @@ -36,6 +36,7 @@ "EXTERNAL_USER": "ulkoinen käyttäjä", "GENERIC_ERROR": "Oompa Loompas havaitsivat virheen {{error}}.", "IOCAINE_TEXT": "Jos tehtävä ahdistaa, merkitse se myrkylliseksi. Ajan mittaa pieninä annoksina saattaa kastokykysi myrkkyä vastaan parantua.", + "CAPSLOCK_WARNING": "Be careful! You're writing with capital letters and this input is case sensitive.", "FORM_ERRORS": { "DEFAULT_MESSAGE": "Tämä arvo vaikuttaa virheelliseltä.", "TYPE_EMAIL": "Tämän pitäisi olla toimiva sähköpostiosoite.", @@ -130,7 +131,8 @@ "POINTS": "Pisteet", "BLOCKED_NOTE": "estetty muistiinpano", "IS_BLOCKED": "on estetty", - "REF": "Ref" + "REF": "Ref", + "VOTES": "Ääniä" }, "ROLES": { "ALL": "Kaikki" @@ -148,11 +150,27 @@ "OPEN": "Avoin" }, "WATCHERS": { - "ADD": "Lisää vahti", - "TITLE": "vahdit", + "ADD": "Add watchers", + "TITLE_ADD": "Add a project member to the watchers list", "DELETE": "Poista vahti", "TITLE_LIGHTBOX_DELETE_WARTCHER": "Poista vahti..." }, + "WATCH_BUTTON": { + "WATCH": "Watch", + "WATCHING": "Seuraa", + "UNWATCH": "Unwatch", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Watch/Unwatch this item", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Upvoted", + "DOWNVOTE": "Downvote", + "VOTERS": "Voters", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "Omat kentät", "SAVE": "Tallenna oma kenttä", @@ -294,7 +312,7 @@ }, "CHANGE_PASSWORD": { "PAGE_TITLE": "Change you password - Taiga", - "PAGE_DESCRIPTION": "Set a new passoword for your Taiga account and hey!, you may want to eat some more iron-rich food, it's good for your brain :P", + "PAGE_DESCRIPTION": "Set a new password for your Taiga account and hey!, you may want to eat some more iron-rich food, it's good for your brain :P", "SECTION_NAME": "Muuta salasanaa", "FIELD_CURRENT_PASSWORD": "Nykyinen salasana", "PLACEHOLDER_CURRENT_PASSWORD": "Nykyinen salasanasi (tai on tyhjä jos sinulla ei vielä ole)", @@ -307,12 +325,10 @@ "CHANGE_PASSWORD_RECOVERY_FORM": { "TITLE": "Luo uusi pääsy Taigaan", "SUBTITLE": "Rautapitoinen ruoka on hyväksi aivoille :P", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Palauta sähköposti-tokeni", - "LINK_NEED_TOKEN": "Tarvitsetko yhden?", - "TITLE_LINK_NEED_TOKEN": "Tarvitsitko tokenia palauttaakseni salasanan koska unohdit sen?", "PLACEHOLDER_NEW_PASSWORD": "Uusi salasana", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Anna salasana uudelleen", "ACTION_RESET_PASSWORD": "Uusi salsanasi", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", "SUCCESS": "Oompa Loompas tallensivat uuden salasanasi.
Yritä kirjautua sisään sillä." }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "Accept the invitation to join a project in Taiga, a project management platform for startups and agile developers & designers who want a simple, beautiful tool that makes work truly enjoyable." }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Ooops, meillä on ongelma
Our Oompa Loompas eivät löydä kutsuasi.", + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", "SUCCESS": "Olet onnistuneesti liittynyt projektiin {{project_name}}. Tervetuloa!", "ERROR": "Oompa Loompas sanovat että käyttäjänimesi tai sähköpostisi tai salasanasi on väärä." }, "HOME": { "PAGE_TITLE": "Home - Taiga", "PAGE_DESCRIPTION": "The Taiga home page with your main projects and all your assigned and watched user stories, tasks and issues", - "EMPTY_WATCHING": "Follow the projects, User Stories, Tasks, Issues... that you want to know about :)", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", "EMPTY_PROJECT_LIST": "You don't have any projects yet", "WORKING_ON_SECTION": "Working on", "WATCHING_SECTION": "Watching" @@ -343,7 +360,7 @@ "DESCRIPTION": "Kirjoita lyhyt kuvaus", "DEPRECATED": "(poistettu)", "DEPRECATED_FILE": "Vanhentunut?", - "ADD": "Lisää liite. <%- maxFileSizeMsg %>", + "ADD": "Add new attachment. {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Maks. koko: {{maxFileSize}}]", "SHOW_DEPRECATED": "+ näytä vanhentuneet liitteet", "HIDE_DEPRECATED": "- piilota vanhentuneet liitteet", @@ -407,15 +424,20 @@ "MEETUP": "Tapaa", "MEETUP_DESCRIPTION": "Valitse videoneuvottelu- järjestelmä. Jopa kehittäjät tarvitsevat katsekontaktia.", "SELECT_VIDEOCONFERENCE": "Valitse videoconferenssi-järjestelmä", - "SALT_CHAT_ROOM": "Voit halutessasi lisätä suolaan chat-huoneen nimeen" + "SALT_CHAT_ROOM": "Voit halutessasi lisätä suolaan chat-huoneen nimeen", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Custom", + "URL_CHAT_ROOM": "URL of your chat room" }, "PROJECT_PROFILE": { "PAGE_TITLE": "{{sectionName}} - Projektin profiili - {{projectName}}", "PROJECT_DETAILS": "Projektin tiedot", "PROJECT_NAME": "Projektin nimi", "PROJECT_SLUG": "Projektin hukka-aika", - "NUMBER_SPRINTS": "Kierrosten määrä", - "NUMBER_US_POINTS": "Kt pisteitä", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", "TAGS": "Avainsanat", "DESCRIPTION": "Kuvaus", "PUBLIC_PROJECT": "Julkinen projekti", @@ -425,7 +447,7 @@ "REPORTS": { "TITLE": "Raportit", "SUBTITLE": "Vie projektisi CSV-tiedostoon", - "DESCRIPTION": "Lataa CSV-tiedosto tai luo URL ja avaa haluamassasi ohjelmassa. Voit visualisoida ja analysoida tietoa helposti.", + "DESCRIPTION": "Download a CSV file or copy the generated URL and open it in your favourite text editor or spreadsheet to make your own project data reports. You will be able to visualize and analyze all your data easily.", "HELP": "Kuinka tätä käytetään omassa taulukossani?", "REGENERATE_TITLE": "Vaihda URL", "REGENERATE_SUBTITLE": "Jos muutata CSV-datan URLia, edellien lakkaa toimimasta. Oletko varma?" @@ -448,7 +470,10 @@ "TASK_DESCRIPTION": "Tehtävien omat kentät", "TASK_ADD": "Lisää omia kenttiä tehtäviin", "ISSUE_DESCRIPTION": "Pyyntöjen omat kentät", - "ISSUE_ADD": "Lisää oma kenttä pyynnöille" + "ISSUE_ADD": "Lisää oma kenttä pyynnöille", + "FIELD_TYPE_TEXT": "Text", + "FIELD_TYPE_MULTI": "Multi-line", + "FIELD_TYPE_DATE": "Date" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - Project values - {{projectName}}", @@ -490,6 +515,9 @@ "PAGE_TITLE": "Roles - {{projectName}}", "WARNING_NO_ROLE": "Ole varovainen, yksikään rooli projektissasi ei voi arvioida käyttäjätarinoidesi kokoa", "HELP_ROLE_ENABLED": "Tämän roolin omaavat jäsenet voivat arvioida käyttäjätarinoiden kokoja", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", "COUNT_MEMBERS": "{{ role.members_count }} jäsentä joilla tämä rooli", "TITLE_DELETE_ROLE": "Poista rooli", "REPLACEMENT_ROLE": "Kaikki käyttäjä joilla on tämä rooli siirretään", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "Aktiivinen", "STATUS_PENDING": "Odottaa", "DELETE_MEMBER": "Poista jäsen", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "Olemme lähettäneet kutsun uudelleen osoitteeseen '{{email}}'.", "ERROR_SEND_INVITATION": "Olemme lähettäneet kutsun.", "SUCCESS_DELETE": "Olemme poistaneet viestin {{message}}.", @@ -626,23 +655,47 @@ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", "EDIT": "Edit profile", "FOLLOW": "Follow", - "PROJECTS": "Projektit", "CLOSED_US": "Closed US", + "PROJECTS": "Projektit", + "PROJECTS_EMPTY": "{{username}} doesn't' have projects yet", "CONTACTS": "Contacts", - "REPORT": "Report Abuse", - "ACTIVITY_TAB": "Activity Tab", - "PROJECTS_TAB": "Projects Tab", - "CONTACTS_TAB": "Contacts Tab", - "FAVORITES_TAB": "Favorites Tab", "CONTACTS_EMPTY": "{{username}} doesn't have contacts yet", "CURRENT_USER_CONTACTS_EMPTY": "You don't have contacts yet", "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "The people with whom you work at Taiga will be your contacts automatically", - "PROJECTS_EMPTY": "{{username}} doesn't' have projects yet" + "REPORT": "Report Abuse", + "TABS": { + "ACTIVITY_TAB": "Aktiivisuus", + "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "PROJECTS_TAB": "Projektit", + "PROJECTS_TAB_TITLE": "List all projects that this user is a member", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Ääniä", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Watched", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Contacts", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } }, "PROFILE_SIDEBAR": { "TITLE": "Your profile", "DESCRIPTION": "People can see everything you do and what are you working on. Add a nice bio to give an enhanced version of your information.", - "ADD_INFO": "Edit your bio" + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Type something...", + "FILTER_TYPE_ALL": "Kaikki", + "FILTER_TYPE_ALL_TITLE": "Show all", + "FILTER_TYPE_PROJECTS": "Projektit", + "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", + "FILTER_TYPE_TASKS": "Tehtävät", + "FILTER_TYPE_TASK_TITLES": "Show only tasks", + "FILTER_TYPE_ISSUES": "Pyynnöt", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." } }, "PROJECT": { @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "Oompa Loompas eivät pysty lukemaan tiedostoasi: {{error_message}}", "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) on liian iso Oompa Loompaseille, kokeile pienemmällä kuin ({{maxFileSize}})", "SYNC_SUCCESS": "Projektisi on tuotu sisään onnistuneesti" + }, + "LIKE_BUTTON": { + "LIKE": "Like", + "LIKED": "Liked", + "UNLIKE": "Unlike", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Watch", + "WATCHING": "Seuraa", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Receive all notifications", + "NOTIFY_ALL_TITLE": "Receive all notifications for this project", + "NOTIFY_INVOLVED": "Only involved", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Unwatch", + "UNWATCH_TITLE": "Unwatch this project" + } } }, "LIGHTBOX": { @@ -783,7 +857,7 @@ "SECTION_NAME": "Käyttäjätarinan tiedot", "LINK_TASKBOARD": "Tehtävätaulu", "TITLE_LINK_TASKBOARD": "Siirry tehtävätauluun", - "TOTAL_POINTS": "yhteensä", + "TOTAL_POINTS": "total points", "ADD": "+ Lisää uusi käyttäjätarina", "ADD_BULK": "Lisää monta käyttäjätarinaa", "PROMOTED": "Tämä Kt on liitetty pyyntöön:", @@ -870,6 +944,10 @@ "PAGE_TITLE": "Backlog - {{projectName}}", "PAGE_DESCRIPTION": "The backlog panel, with user stories and sprints of the project {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Odottavat", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Hallinnoi", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "Siirrä nykyiseen kierrokseen", "SHOW_FILTERS": "Näytä suodattimet", "SHOW_TAGS": "Näytä avainsanat", @@ -889,10 +967,10 @@ "CHART": { "XAXIS_LABEL": "Kierrokset", "YAXIS_LABEL": "Pisteet", - "OPTIMAL": "Optimaaliset odottavat pisteet kierokselle {{xval}} ovat {{yval}}", - "REAL": "Todelliset odottavat pisteet kierrokselle {{xval}} ovat {{yval}}", - "INCREMENT_TEAM": "Lisätyt tiimin vaatimat pisteet kierrokselle {{xval}} ovat {{yval}}", - "INCREMENT_CLIENT": "Lisätyt asiakkaan vaatimat pisteet kierrokselle {{xval}} ovat {{yval}}" + "OPTIMAL": "Optimal pending points for sprint \"{{sprintName}}\" should be {{value}}", + "REAL": "Real pending points for sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_TEAM": "Incremented points by team requirements for sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_CLIENT": "Incremented points by client requirements for sprint \"{{sprintName}}\" is {{value}}" }, "TAGS": { "TOGGLE": "Vaihda avainsanojen näkyvyyttä", @@ -909,7 +987,8 @@ "OPEN_TASKS": "avaa
tehtävät", "CLOSED_TASKS": "suljettu
tehtävää", "IOCAINE_DOSES": "myrkkye-
annosta", - "SHOW_STATISTICS_TITLE": "Näytä tilastot" + "SHOW_STATISTICS_TITLE": "Näytä tilastot", + "TOGGLE_BAKLOG_GRAPH": "Show/Hide burndown graph" }, "SUMMARY": { "PROJECT_POINTS": "projekti
pistettä", @@ -932,19 +1011,20 @@ "LINK_TASKBOARD": "Kierroksien tehtävätaulu", "TITLE_LINK_TASKBOARD": "Siirry tehtävätauluun {{name}}", "NUMBER_SPRINTS": "
kierroksia", - "TITLE_ACTION_NEW_SPRINT": "+ Uusi kierros", - "ACTION_NEW_SPRINT": "+ Uusi kierros", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", "ACTION_SHOW_CLOSED_SPRINTS": "Näytä suljetut kierrokset", "ACTION_HIDE_CLOSED_SPRINTS": "Piilota suljetut kierrokset" } }, "ERROR": { "TEXT1": "Jotain tapahtui ja Oompa Loompas työskentelevät sen parissa.", - "TEXT2": "Lataa sivu uudestaan", "NOT_FOUND": "Ei löytynyt", "NOT_FOUND_TEXT": "Virhe 404. Sivua ei löydy. Palaa takaisin TAIGA etusivulle ja katso löydätkö haluamasi sieltä.", "PERMISSION_DENIED": "Ei oikeutta", - "PERMISSION_DENIED_CODE": "Virhe 403.", + "PERMISSION_DENIED_TEXT": "You don't have permission to access to this page.", "VERSION_ERROR": "Joku Taigassa on päivittänyt tätä ennen sinua. Muutoksiasi ei voida tallentaa. Lataa sivu uudestaan ja korjaa tilanne." }, "TASKBOARD": { @@ -955,6 +1035,8 @@ "TITLE_ACTION_ADD_BULK": "Lisää monta tehtävää", "TITLE_ACTION_ASSIGN": "Valitse tekijä", "TITLE_ACTION_EDIT": "Muokkaa tehtävää", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", "TABLE": { "COLUMN": "Käyttäjätarina", "TITLE_ACTION_FOLD": "Kavenna sarake", @@ -1073,16 +1155,17 @@ "SEVERITY": "Vakavuus", "PRIORITY": "Tärkeys", "SUBJECT": "Aihe", + "VOTES": "Ääniä", "STATUS": "Tila", "CREATED": "Luotu", "ASSIGNED_TO": "Tekijä" }, "TITLE_ACTION_CHANGE_STATUS": "Muuta tilaa", "TITLE_ACTION_ASSIGNED_TO": "Tekijä", + "BLOCKED": "Suljettu", "EMPTY": { "TITLE": "Ei raportoitavia pyyntöjä:-)", - "SUBTITLE": "Löysitkö ongelman?", - "ACTION_CREATE_ISSUE": "Luo uusi pyyntö" + "SUBTITLE": "Löysitkö ongelman?" } } }, @@ -1104,7 +1187,9 @@ "ACTION_HIDE_ARCHIVED": "Piilota arkisto", "HIDDEN_USER_STORIES": "Käyttäjätarinat tällä alueella ovat oletuksena piilotettuna", "ARCHIVED": "Olet arkistoitu", - "UNDO_ARCHIVED": "Raahaa ja pudota uudelleen peruaksesi" + "UNDO_ARCHIVED": "Raahaa ja pudota uudelleen peruaksesi", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" }, "SEARCH": { "PAGE_TITLE": "Search - {{projectName}}", @@ -1182,7 +1267,9 @@ "BIO": "Bio (max. 210 chars)", "PLACEHOLDER_BIO": "Kerro jotain itsestäsi", "LANGUAGE": "Kieli", - "LANGUAGE_DEFAULT": "-- käytä oletuskieltä --" + "LANGUAGE_DEFAULT": "-- käytä oletuskieltä --", + "THEME": "Theme", + "THEME_DEFAULT": "-- use default theme --" } }, "WIZARD": { @@ -1234,18 +1321,91 @@ "NEW_PROJECT": "{{username}} created the project {{project_name}}", "MILESTONE_UPDATED": "{{username}} has updated the sprint {{obj_name}}", "US_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the US {{obj_name}} to {{new_value}}", + "US_UPDATED_POINTS": "{{username}} has updated '{{role_name}}' points of the US {{obj_name}} to {{new_value}}", "ISSUE_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the issue {{obj_name}}", - "TASK_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the issue {{obj_name}} to {{new_value}}", + "TASK_UPDATED": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} to {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} to {{new_value}}", "TASK_UPDATED_WITH_US": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} has updated the attribute \"{{field_name}}\" of the task {{obj_name}} which belongs to the US {{us_name}} to {{new_value}}", "WIKI_UPDATED": "{{username}} has updated the wiki page {{obj_name}}", "NEW_COMMENT_US": "{{username}} has commented in the US {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} has commented in the issue {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} has commented in the task {{obj_name}}", "NEW_MEMBER": "{{project_name}} has a new member", "US_ADDED_MILESTONE": "{{username}} has added the US {{obj_name}} to {{sprint_name}}", + "US_MOVED": "{{username}} has moved the US {{obj_name}}", "US_REMOVED_FROM_MILESTONE": "{{username}} has added the US {{obj_name}} to the backlog", "BLOCKED": "{{username}} has blocked {{obj_name}}", "UNBLOCKED": "{{username}} has unblocked {{obj_name}}", "NEW_USER": "{{username}} has joined Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "An external app requires authentication", + "PAGE_DESCRIPTION": "An external app requires authentication", + "AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?", + "LOGIN_WITH_ANOTHER_USER": "Login with another user", + "AUTHORIZE_APP": "Authorize app", + "CANCEL": "Peru" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Projektisi", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Working on", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Seuraa", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Kierrokset", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "Käyttäjätarinat", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } } } \ No newline at end of file diff --git a/app/locales/locale-fr.json b/app/locales/locale-fr.json index 2ddd3a25..d76d0ea8 100644 --- a/app/locales/locale-fr.json +++ b/app/locales/locale-fr.json @@ -2,7 +2,7 @@ "COMMON": { "YES": "Oui", "NO": "Non", - "LOADING": "Chargement...", + "LOADING": "Veuillez patienter...", "LOADING_PROJECT": "Chargement du projet...", "DATE": "DD MMM YYYY", "DATETIME": "DD MMM YYYY HH:mm", @@ -36,6 +36,7 @@ "EXTERNAL_USER": "um usuário externo", "GENERIC_ERROR": "L'un de nos Oompa Loompas dit {{error}}.", "IOCAINE_TEXT": "Vous vous sentez un peu submergé par une tâche ? Soyez certains d'en informer les autres en cliquant su Iocaine lors de la modification de la tâche. Il est possible de s'immuniser contre ce poison (fictif) en consommant de petites quantités en heures supplémentaires, tout comme il est possible de s'améliorer en acceptant parfois de nouveaux défis !", + "CAPSLOCK_WARNING": "Be careful! You're writing with capital letters and this input is case sensitive.", "FORM_ERRORS": { "DEFAULT_MESSAGE": "Cette valeur semble être invalide.", "TYPE_EMAIL": "Cette valeur devrait être une adresse courriel valide.", @@ -103,7 +104,7 @@ "SEE_USER_PROFILE": "Afficher le profil de {{username}}", "USER_STORY": "Récit utilisateur", "TASK": "Tâche", - "ISSUE": "Suivi de problème", + "ISSUE": "Ticket", "TAGS": { "PLACEHOLDER": "Taggez moi !", "DELETE": "Supprimer le mot-clé", @@ -130,7 +131,8 @@ "POINTS": "Points", "BLOCKED_NOTE": "note bloquée", "IS_BLOCKED": "est bloqué", - "REF": "Réf." + "REF": "Réf.", + "VOTES": "Votes" }, "ROLES": { "ALL": "Tout" @@ -148,11 +150,27 @@ "OPEN": "Ouvert" }, "WATCHERS": { - "ADD": "Ajouter un observateur", - "TITLE": "Observateurs", + "ADD": "Add watchers", + "TITLE_ADD": "Add a project member to the watchers list", "DELETE": "Supprimer l'observateur", "TITLE_LIGHTBOX_DELETE_WARTCHER": "Supprimer l'observateur..." }, + "WATCH_BUTTON": { + "WATCH": "Watch", + "WATCHING": "Observant", + "UNWATCH": "Unwatch", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Watch/Unwatch this item", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Upvoted", + "DOWNVOTE": "Downvote", + "VOTERS": "Voters", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "Champs personnalisés", "SAVE": "Sauver le champ personnalisé", @@ -220,11 +238,11 @@ "DELETE_TASKS": "Supprimer des tâches" }, "ISSUES": { - "NAME": "Problèmes", - "VIEW_ISSUES": "Voir les suivis de problèmes", - "ADD_ISSUES": "Ajouter des suivis de problèmes", - "MODIFY_ISSUES": "Modifier des suivis de problèmes", - "DELETE_ISSUES": "Supprimer des suivis de problèmes" + "NAME": "Tickets", + "VIEW_ISSUES": "Voir les tickets", + "ADD_ISSUES": "Ajouter des tickets", + "MODIFY_ISSUES": "Modifier des tickets", + "DELETE_ISSUES": "Supprimer des tickets" }, "WIKI": { "NAME": "Wiki", @@ -253,7 +271,7 @@ "CREATE_ACCOUNT": "créer votre compte gratuit ici" }, "LOGIN_COMMON": { - "HEADER": "J'ai déjà un login Taiga", + "HEADER": "J'ai déjà un identifiant Taiga", "PLACEHOLDER_AUTH_NAME": "Nom d'utilisateur ou adresse courriel (sensible à la casse)", "LINK_FORGOT_PASSWORD": "Mot de passe oublié?", "TITLE_LINK_FORGOT_PASSWORD": "Avez-vous oublié votre mot de passe ?", @@ -288,7 +306,7 @@ "SUBTITLE": "Saisissez votre nom d'utilisateur ou votre adresse courriel pour en obtenir un nouveau", "PLACEHOLDER_FIELD": "Nom d'utilisateur ou adresse courriel", "ACTION_RESET_PASSWORD": "Réinitialiser le mot de passe", - "LINK_CANCEL": "Nan, ramenez-moi en arrière. Je crois que je m'en rappelle.", + "LINK_CANCEL": "Nan, ramenez-moi en arrière. Je crois que je m'en souviens.", "SUCCESS": "Consultez votre messagerie!
Nous venons d'envoyer un courriel avec les instructions pour créer un nouveau mot de passe", "ERROR": "D'après nos Oompa Loompas, vous n'êtes pas encore enregistré." }, @@ -307,12 +325,10 @@ "CHANGE_PASSWORD_RECOVERY_FORM": { "TITLE": "Créez un nouveau mot de passe pour Taiga", "SUBTITLE": "Et au fait, vous devriez penser à manger de la nourriture plus riche en fer, c'est bon pour le cerveau. :p", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Récupérer le jeton du mot de passe", - "LINK_NEED_TOKEN": "En avez-vous besoin?", - "TITLE_LINK_NEED_TOKEN": "Avez-vous besoin d'un token pour récupérer votre mot de passe que vous avez oublié ?", "PLACEHOLDER_NEW_PASSWORD": "Nouveau mot de passe", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Confirmez votre nouveau mot de passe", "ACTION_RESET_PASSWORD": "Réinitialiser le mot de passe", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", "SUCCESS": "Nos Oompa Loompas ont sauvegardé votre nouveau mot de passe.
Essayez de vous connecter avec." }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "Acceptez l'invitation à rejoindre un projet sur Taiga, une plateforme de gestion de projets pour les startups, les développeurs et les designers agiles qui veulent un outil simple, beau et qui rend le travail vraiment agréable." }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Oh là là, nous avons un problème
Nos Oompa Loompas trouvent pas votre invitation.", + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", "SUCCESS": "Vous avez rejoint ce projet, Bienvenue sur {{project_name}}", "ERROR": "D'après nos Oompa Loompas, vous n'êtes pas encore enregistrés ou vous avez saisi un mot de passe incorrect." }, "HOME": { "PAGE_TITLE": "Accueil - Taiga", - "PAGE_DESCRIPTION": "La page d'accueil de Taiga sur laquelle apparaissent vos projets principaux et toutes les récits utilisateur, tâches et suivis de problèmes qui vont sont assignés et surveillés.", - "EMPTY_WATCHING": "Suivre les projets, Récits Utilisateur, Tâches, Problèmes... que vous voulez connaître :)", + "PAGE_DESCRIPTION": "La page d'accueil de Taiga sur laquelle apparaissent vos projets principaux et toutes les récits utilisateur, tâches et tickets qui vont sont assignés et surveillés.", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", "EMPTY_PROJECT_LIST": "Vous n'avez aucun projet pour l'instant", "WORKING_ON_SECTION": "Projets en cours", "WATCHING_SECTION": "Observant" @@ -343,7 +360,7 @@ "DESCRIPTION": "Saisissez une courte description", "DEPRECATED": "(obsolète)", "DEPRECATED_FILE": "Obsolète?", - "ADD": "Ajouter une pièce jointe. <%- maxFileSizeMsg %>", + "ADD": "Add new attachment. {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Taille max.: {{maxFileSize}}]", "SHOW_DEPRECATED": "+ montrer les pièces jointes obsolètes", "HIDE_DEPRECATED": "- cacher les pièces jointes obsolètes", @@ -400,22 +417,27 @@ "BACKLOG_DESCRIPTION": "Gérez votre récits utilisateur pour garder une vue organisée des travaux à venir et priorisés.", "KANBAN": "Kanban", "KANBAN_DESCRIPTION": "Organisez votre projet de manière agile avec ce tableau.", - "ISSUES": "Suivis de problèmes", + "ISSUES": "Tickets", "ISSUES_DESCRIPTION": "Suivez les bugs, questions et améliorations liés à votre projet. Ne ratez rien !", "WIKI": "Wiki", "WIKI_DESCRIPTION": "Ajoutez, modifiez, ou supprimez du contenu en collaboration avec d'autres. C'est le bon endroit pour la documentation de votre projet.", "MEETUP": "Meet Up", "MEETUP_DESCRIPTION": "Choisissez votre système de vidéoconférence. Même les développeurs ont besoin de contact en face à face.", "SELECT_VIDEOCONFERENCE": "Choisissez un système de vidéoconférence", - "SALT_CHAT_ROOM": "Si vous le souhaitez vous pouvez ajouter un code de salage au nom du salon de discussion" + "SALT_CHAT_ROOM": "Si vous le souhaitez vous pouvez ajouter un code de salage au nom du salon de discussion", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Personnalisé", + "URL_CHAT_ROOM": "URL du salon de discussion" }, "PROJECT_PROFILE": { "PAGE_TITLE": "{{sectionName}} - Profil projet - {{projectName}}\n", "PROJECT_DETAILS": "Détails du projet", "PROJECT_NAME": "Nom du projet", "PROJECT_SLUG": "Label du projet", - "NUMBER_SPRINTS": "Nombre de sprints", - "NUMBER_US_POINTS": "Nombre de points du récit utilisateur", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", "TAGS": "Mots-clés", "DESCRIPTION": "Description", "PUBLIC_PROJECT": "Projet public", @@ -425,7 +447,7 @@ "REPORTS": { "TITLE": "Rapports", "SUBTITLE": "Exportez les données de votre projet au format CSV pour créer vos propres rapports.", - "DESCRIPTION": "Téléchargez un fichier CSV ou copiez l'URL générée et ouvrez-là dans votre éditeur de texte ou feuille de calcul favorite pour créer vos propres rapports de projet. Vous pourrez visualiser et analyser toutes vos données facilement.", + "DESCRIPTION": "Download a CSV file or copy the generated URL and open it in your favourite text editor or spreadsheet to make your own project data reports. You will be able to visualize and analyze all your data easily.", "HELP": "Comment utiliser ceci dans ma propre feuille de calcul?", "REGENERATE_TITLE": "Changer l'URL", "REGENERATE_SUBTITLE": "Vous êtes sur le point de changer l'url d'accès aux données CSV. L'url précédente sera désactivée. Êtes-vous sûre ?" @@ -433,7 +455,7 @@ "CSV": { "SECTION_TITLE_US": "Rapports des récits utilisateur", "SECTION_TITLE_TASK": "rapports des tâches", - "SECTION_TITLE_ISSUE": "Rapports des suivis de problèmes", + "SECTION_TITLE_ISSUE": "Rapports des tickets", "DOWNLOAD": "Télécharger au format CSV", "URL_FIELD_PLACEHOLDER": "Merci de regénérer l'url de téléchargement au format CSV", "TITLE_REGENERATE_URL": "Regénérer l'URL du CSV", @@ -442,13 +464,16 @@ }, "CUSTOM_FIELDS": { "TITLE": "Champs Personnalisés", - "SUBTITLE": "Spécifiez les champs personnalisés de vos récits utilisateur, tâches et problèmes", + "SUBTITLE": "Spécifiez les champs personnalisés de vos récits utilisateur, tâches et tickets", "US_DESCRIPTION": "Champs personnalisés des récits utilisateur", "US_ADD": "Ajouter un champ personnalisé dans les récits utilisateur", "TASK_DESCRIPTION": "Champs personnalisés de tâches", "TASK_ADD": "Ajouter un champ personnalisé dans les tâches", - "ISSUE_DESCRIPTION": "Champs personnalisés des problèmes", - "ISSUE_ADD": "Ajouter un champ personnalisé dans les problèmes" + "ISSUE_DESCRIPTION": "Champs personnalisés des tickets", + "ISSUE_ADD": "Ajouter un champ personnalisé dans les tickets", + "FIELD_TYPE_TEXT": "Texte", + "FIELD_TYPE_MULTI": "Multiligne", + "FIELD_TYPE_DATE": "Date" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - Valeurs du projet - {{projectName}}", @@ -463,33 +488,36 @@ }, "PROJECT_VALUES_PRIORITIES": { "TITLE": "Priorités", - "SUBTITLE": "Spécifiez les priorités qu'auront vos suivis de problèmes", - "ISSUE_TITLE": "Priorités de problèmes", + "SUBTITLE": "Spécifiez les priorités qu'auront vos tickets", + "ISSUE_TITLE": "Priorités de tickets", "ACTION_ADD": "Ajouter une priorité" }, "PROJECT_VALUES_SEVERITIES": { "TITLE": "Sévérités", - "SUBTITLE": "Spécifiez les sévérités qu'auront vos problèmes", - "ISSUE_TITLE": "Sévérités des suivis de problèmes", + "SUBTITLE": "Spécifiez les sévérités qu'auront vos tickets", + "ISSUE_TITLE": "Sévérités des tickets", "ACTION_ADD": "Ajouter un degré de sévérité" }, "PROJECT_VALUES_STATUS": { "TITLE": "Statut", - "SUBTITLE": "Spécifiez les statuts que vont prendre vos récits utilisateur, tâches et suivis de problèmes", + "SUBTITLE": "Spécifiez les statuts que vont prendre vos récits utilisateur, tâches et tickets", "US_TITLE": "Statuts des HU", "TASK_TITLE": "Statuts des Tâches", - "ISSUE_TITLE": "Statuts des Problèmes" + "ISSUE_TITLE": "Statuts des Tickets" }, "PROJECT_VALUES_TYPES": { "TITLE": "Types", "SUBTITLE": "Spécifiez les priorités qu'auront vos bugs", - "ISSUE_TITLE": "Types de problèmes", + "ISSUE_TITLE": "Types de tickets", "ACTION_ADD": "Ajouter un nouveau {{objName}}" }, "ROLES": { "PAGE_TITLE": "Rôles - {{projectName}}", "WARNING_NO_ROLE": "Attention, aucun rôle dans votre projet ne pourra estimer la valeur du point pour les récits utilisateurs", "HELP_ROLE_ENABLED": "Si activé, les membres affectés à ce rôle pourront estimer la valeur du point pour les récits utilisateurs", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", "COUNT_MEMBERS": "{{ role.members_count }} membres avec ce rôle", "TITLE_DELETE_ROLE": "Supprimer des rôles", "REPLACEMENT_ROLE": "Tous les utilisateurs avec ce rôle seront déplacés dans", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "Actif", "STATUS_PENDING": "En attente", "DELETE_MEMBER": "Supprimer un membre", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "Nous avons envoyé une autre invitation à '{{email}}'.", "ERROR_SEND_INVITATION": "Nous n'avons pas envoyé l'invitation", "SUCCESS_DELETE": "Nous avons supprimé {{message}}.", @@ -626,23 +655,47 @@ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", "EDIT": "Modifier le profil", "FOLLOW": "Suivre", - "PROJECTS": "Projets", "CLOSED_US": "Récit utilisateur fermé", + "PROJECTS": "Projets", + "PROJECTS_EMPTY": "{{username}} n'a aucun projet pour l'instant", "CONTACTS": "Contacts", - "REPORT": "Signaler un abus", - "ACTIVITY_TAB": "Onglet d'activités", - "PROJECTS_TAB": "Onglet des projets", - "CONTACTS_TAB": "Onglet des contacts", - "FAVORITES_TAB": "Onglets des favoris", "CONTACTS_EMPTY": "{{username}} n'a aucun contact pour l'instant", "CURRENT_USER_CONTACTS_EMPTY": "Vous n'avez aucun contact pour l'instant", "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Les personnes avec qui vous travaillez à Taiga seront vos contacts automatiquement", - "PROJECTS_EMPTY": "{{username}} n'a aucun projet pour l'instant" + "REPORT": "Signaler un abus", + "TABS": { + "ACTIVITY_TAB": "Activité", + "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "PROJECTS_TAB": "Projets", + "PROJECTS_TAB_TITLE": "List all projects that this user is a member", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Votes", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Watched", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Contacts", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } }, "PROFILE_SIDEBAR": { "TITLE": "Votre profil", "DESCRIPTION": "Tout le monde peut voir ce que vous faites et ce sur quoi vous travaillez. Ajoutez une bio détaillée pour améliorer la vision perçue de votre profil.", - "ADD_INFO": "Modifier votre biographie" + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Type something...", + "FILTER_TYPE_ALL": "Toutes", + "FILTER_TYPE_ALL_TITLE": "Show all", + "FILTER_TYPE_PROJECTS": "Projets", + "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", + "FILTER_TYPE_TASKS": "Tâches", + "FILTER_TYPE_TASK_TITLES": "Show only tasks", + "FILTER_TYPE_ISSUES": "Tickets", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." } }, "PROJECT": { @@ -662,7 +715,7 @@ "TIMELINE": "Chronologie", "BACKLOG": "Backlog", "KANBAN": "Kanban", - "ISSUES": "Suivis de problèmes", + "ISSUES": "Tickets", "WIKI": "Wiki", "TEAM": "Équipe", "MEETUP": "Meet up", @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "Nos Oompa Loompas ont des problèmes pour importer vos données de vidage : {{error_message}}", "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) est un peu trop lourd pour nos Oompa Loompas, réessayez avec un fichier d'une taille inférieure à ({{maxFileSize}})", "SYNC_SUCCESS": "Votre projet a été importé avec succès" + }, + "LIKE_BUTTON": { + "LIKE": "Like", + "LIKED": "Liked", + "UNLIKE": "Unlike", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Watch", + "WATCHING": "Observant", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Receive all notifications", + "NOTIFY_ALL_TITLE": "Receive all notifications for this project", + "NOTIFY_INVOLVED": "Only involved", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Unwatch", + "UNWATCH_TITLE": "Unwatch this project" + } } }, "LIGHTBOX": { @@ -723,7 +797,7 @@ "DELETE_PROJECT": { "TITLE": "Supprimer le projet", "QUESTION": "Êtes-vous sûr de vouloir supprimer ce projet?", - "SUBTITLE": "Toutes les données ( récit utilisateur, tâches, problèmes, sprints et pages de wiki) seront perdues! :-(", + "SUBTITLE": "Toutes les données ( récit utilisateur, tâches, tickets, sprints et pages de wiki) seront perdues! :-(", "CONFIRM": "Oui, je suis sûr" }, "ASSIGNED_TO": { @@ -735,7 +809,7 @@ "HELP_TEXT": "Si vos utilisateurs sont déjà inscrits sur Taiga, ils seront automatiquement ajoutés. Sinon, ils recevront une invitation." }, "CREATE_ISSUE": { - "TITLE": "Ajouter un suivi de problème" + "TITLE": "Ajouter un ticket" }, "FEEDBACK": { "TITLE": "Dites nous quelque chose...", @@ -783,11 +857,11 @@ "SECTION_NAME": "Détails du récit utilisateur", "LINK_TASKBOARD": "Tableau des tâches", "TITLE_LINK_TASKBOARD": "Aller au tableau des tâches", - "TOTAL_POINTS": "total", + "TOTAL_POINTS": "total points", "ADD": "Ajouter un nouveau récit utilisateur", "ADD_BULK": "Ajouter de nouveaux récits utilisateur en lot", - "PROMOTED": "Ce récit utilisateur a été promue à partir d'un suivi de problème :", - "TITLE_LINK_GO_TO_ISSUE": "Aller vers ce suivi de problème", + "PROMOTED": "Ce récit utilisateur a été promue à partir d'un ticket :", + "TITLE_LINK_GO_TO_ISSUE": "Aller vers ce ticket", "EXTERNAL_REFERENCE": "Ce récit utilisateur a été créé depuis", "GO_TO_EXTERNAL_REFERENCE": "Allez à l'origine", "BLOCKED": "Ce récit utilisateur est bloqué", @@ -870,6 +944,10 @@ "PAGE_TITLE": "Carnet - {{projectName}}", "PAGE_DESCRIPTION": "Le panneau carnet avec les récits utilisateurs et sprints du projet {{projectName}} : {{projectDescription}}", "SECTION_NAME": "Carnet", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Admin", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "Déplacer vers le Sprint Courant", "SHOW_FILTERS": "Afficher les filtres", "SHOW_TAGS": "Afficher les mots-clés", @@ -889,10 +967,10 @@ "CHART": { "XAXIS_LABEL": "Sprints", "YAXIS_LABEL": "Points", - "OPTIMAL": "Le nombre optimal de points en attente pour le sprint {{xval}} devrait être {{yval}}", - "REAL": "Le nombre réel de points pour le sprint {{xval}} est {{yval}}", - "INCREMENT_TEAM": "Le nombre de points ajoutés par les exigences de l'équipe pour le sprint {{xval}} est {{yval}}", - "INCREMENT_CLIENT": "Le nombre de points ajoutés par les exigences du client pour le sprint {{xval}} est {{yval}}" + "OPTIMAL": "Le nombre optimal de points pour le sprint \"{{sprintName}}\" devrait être {{value}}", + "REAL": "Le nombre réel de points pour le sprint \"{{sprintName}}\" est {{value}}", + "INCREMENT_TEAM": "Le nombre de points ajoutés par les exigences de l'équipe pour le sprint \"{{sprintName}}\" est {{value}}", + "INCREMENT_CLIENT": "Le nombre de points ajoutés par les exigences du client pour le sprint \"{{sprintName}}\" est {{value}}" }, "TAGS": { "TOGGLE": "Afficher/Cacher les tags", @@ -909,7 +987,8 @@ "OPEN_TASKS": "tâches
ouvertes", "CLOSED_TASKS": "tâches
fermées", "IOCAINE_DOSES": "doses
de iocaine", - "SHOW_STATISTICS_TITLE": "Afficher les statistiques" + "SHOW_STATISTICS_TITLE": "Afficher les statistiques", + "TOGGLE_BAKLOG_GRAPH": "Afficher/masquer le graphique d'avancement" }, "SUMMARY": { "PROJECT_POINTS": "projet
points", @@ -932,19 +1011,20 @@ "LINK_TASKBOARD": "Taskboard du sprint", "TITLE_LINK_TASKBOARD": "Aller au Taskboard de \"{{name}}\"", "NUMBER_SPRINTS": "
sprints", - "TITLE_ACTION_NEW_SPRINT": "+ Nouveau sprint", - "ACTION_NEW_SPRINT": "+ Nouveau sprint", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", "ACTION_SHOW_CLOSED_SPRINTS": "Afficher les sprints fermés", "ACTION_HIDE_CLOSED_SPRINTS": "Cacher les sprints fermés" } }, "ERROR": { "TEXT1": "Un problème s'est produit et nos Oompa Loompas y travaillent.", - "TEXT2": "Veuillez essayer de rafraîchir dans quelques instants.", "NOT_FOUND": "Non trouvé", "NOT_FOUND_TEXT": "Erreur 404. La page que vous cherchez n'existe plus. Peut-être pouvez-vous retourner à la page d'accueil de Taiga et voir si vous trouvez ce que vous cherchez.", "PERMISSION_DENIED": "Permission refusée", - "PERMISSION_DENIED_CODE": "Erreur 403.", + "PERMISSION_DENIED_TEXT": "Vous n'avez pas la permission d'accéder à cette page.", "VERSION_ERROR": "Quelqu'un a changé ça auparavant dans Taiga et nos Oompa Loompas ne peuvent appliquer vos modifications. Veuillez recharger la page et appliquer vos modifications de nouveaux (elles seront perdues)." }, "TASKBOARD": { @@ -955,6 +1035,8 @@ "TITLE_ACTION_ADD_BULK": "Ajouter de nouvelles tâches en lot", "TITLE_ACTION_ASSIGN": "Affecter une tâche", "TITLE_ACTION_EDIT": "Modifier la tâche", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", "TABLE": { "COLUMN": "Récit utilisateur", "TITLE_ACTION_FOLD": "Replier la colonne", @@ -1021,29 +1103,29 @@ "SUCCESS": "Nos Oompa Loompas ont mis à jour votre adresse courriel" }, "ISSUES": { - "PAGE_TITLE": "Problèmes - {{projectName}}", - "PAGE_DESCRIPTION": "Le panneau de la liste des problèmes du projet {{projectName}} : {{projectDescription}}", - "LIST_SECTION_NAME": "Problèmes", - "SECTION_NAME": "Détails du problème", - "ACTION_NEW_ISSUE": "+ NOUVEAU PROBLÈME", + "PAGE_TITLE": "Tickets - {{projectName}}", + "PAGE_DESCRIPTION": "Le panneau de la liste des tickets du projet {{projectName}} : {{projectDescription}}", + "LIST_SECTION_NAME": "Tickets", + "SECTION_NAME": "Détails du ticket", + "ACTION_NEW_ISSUE": "+ NOUVEAU TICKET", "ACTION_PROMOTE_TO_US": "Transformer en User Story", "PLACEHOLDER_FILTER_NAME": "Écrivez le nom du filtre et appuyez sur \"Entrée\"", - "PROMOTED": "Le problème a été promu en récit utilisateur", - "EXTERNAL_REFERENCE": "Ce problème a été créé à partir de", + "PROMOTED": "Le ticket a été promu en récit utilisateur", + "EXTERNAL_REFERENCE": "Ce ticket a été créé à partir de", "GO_TO_EXTERNAL_REFERENCE": "Aller à l'origine", "BLOCKED": "Ce bug est bloqué", "TITLE_PREVIOUS_ISSUE": "bug précédent", - "TITLE_NEXT_ISSUE": "problème suivant", - "ACTION_DELETE": "Supprimer le problème", - "LIGHTBOX_TITLE_BLOKING_ISSUE": "Problème bloquant", + "TITLE_NEXT_ISSUE": "ticket suivant", + "ACTION_DELETE": "Supprimer le ticket", + "LIGHTBOX_TITLE_BLOKING_ISSUE": "Ticket bloquant", "FIELDS": { "PRIORITY": "Priorité", "SEVERITY": "Sévérité", "TYPE": "Type" }, "CONFIRM_PROMOTE": { - "TITLE": "Transformer ce problème en une nouvelle user story", - "MESSAGE": "Etes-vous sure de vouloir créer un nouvelle US à partir de ce problème ?" + "TITLE": "Transformer ce ticket en un nouveau récit utilisateur", + "MESSAGE": "Êtes-vous sure de vouloir créer un nouveau récit utilisateur à partir de ce ticket ?" }, "FILTERS": { "TITLE": "Filtres", @@ -1073,21 +1155,22 @@ "SEVERITY": "Sévérité", "PRIORITY": "Priorité", "SUBJECT": "Objet", + "VOTES": "Votes", "STATUS": "Statut", "CREATED": "Créé le", "ASSIGNED_TO": "Affecté à" }, "TITLE_ACTION_CHANGE_STATUS": "Changer le statut", "TITLE_ACTION_ASSIGNED_TO": "Affecté à", + "BLOCKED": "Bloqué", "EMPTY": { - "TITLE": "Aucun problème rapporté :-)", - "SUBTITLE": "Avez-vous trouvé un problème ?", - "ACTION_CREATE_ISSUE": "Créer un nouveau problème" + "TITLE": "Aucun ticket rapporté :-)", + "SUBTITLE": "Avez-vous trouvé un problème ?" } } }, "ISSUE": { - "PAGE_TITLE": "{{issueSubject}} - Problème {{issueRef}} - {{projectName}}", + "PAGE_TITLE": "{{issueSubject}} - Ticket {{issueRef}} - {{projectName}}", "PAGE_DESCRIPTION": "État : {{issueStatus }}. Type : {{issueType}}, Priorité : {{issuePriority}}. Sévérité : {{issueSeverity}}. Description : {{issueDescription}}" }, "KANBAN": { @@ -1104,13 +1187,15 @@ "ACTION_HIDE_ARCHIVED": "Cacher les éléments archivés", "HIDDEN_USER_STORIES": "Les user stories avec ce statut sont masquées par défaut", "ARCHIVED": "Vous avez archivé", - "UNDO_ARCHIVED": "Refaites le glisser-déposer pour annuler" + "UNDO_ARCHIVED": "Refaites le glisser-déposer pour annuler", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" }, "SEARCH": { "PAGE_TITLE": "Chercher - {{projectName}}", - "PAGE_DESCRIPTION": "Chercher tout, récits utilisateurs, problèmes, tâches ou pages de wiki, dans le projet {{projectName}} : {{projectDescription}}", + "PAGE_DESCRIPTION": "Chercher tout, récits utilisateurs, tickets, tâches ou pages de wiki, dans le projet {{projectName}} : {{projectDescription}}", "FILTER_USER_STORIES": "Récits utilisateur", - "FILTER_ISSUES": "Problèmes", + "FILTER_ISSUES": "Tickets", "FILTER_TASKS": "Tâches", "FILTER_WIKI": "Pages Wiki", "PLACEHOLDER_SEARCH": "Rechercher dans...", @@ -1131,7 +1216,7 @@ "COLUMN_CERVANTES": "Cervantes", "EXPLANATION_COLUMN_CERVANTES": "Page Wiki éditée", "COLUMN_BUG_HUNTER": "Chasseur de bug", - "EXPLANATION_COLUMN_BUG_HUNTER": "Problèmes rapportés", + "EXPLANATION_COLUMN_BUG_HUNTER": "Tickets rapportés", "COLUMN_NIGHT_SHIFT": "Equipe de nuit", "EXPLANATION_COLUMN_NIGHT_SHIFT": "Tâches fermées", "COLUMN_TOTAL_POWER": "Puissance totale", @@ -1182,7 +1267,9 @@ "BIO": "Biographie (maximum 210 caractères)", "PLACEHOLDER_BIO": "Dites en nous plus sur vous", "LANGUAGE": "Langue", - "LANGUAGE_DEFAULT": "-- utiliser la langue par défaut --" + "LANGUAGE_DEFAULT": "-- utiliser la langue par défaut --", + "THEME": "Thème", + "THEME_DEFAULT": "-- Utiliser la langue par défaut --" } }, "WIZARD": { @@ -1221,12 +1308,12 @@ "HINT3_TITLE": "Triez vos projets pour mettre en avant ceux qui sont les plus pertinants pour vous", "HINT3_TEXT": "Vos 10 principaux projets seront accessibles via la barre du haut", "HINT4_TITLE": "Avez-vous oublié ce sur quoi vous travailliez ?", - "HINT4_TEXT": "Pas d'inquiétude, vous trouverez dans votre tableau de bord vos tâches, problèmes et récits utilisateurs dans l'ordre dans lequel vous avez travaillé dessus." + "HINT4_TEXT": "Pas d'inquiétude, vous trouverez dans votre tableau de bord vos tâches, tickets et récits utilisateurs dans l'ordre dans lequel vous avez travaillé dessus." }, "TIMELINE": { "UPLOAD_ATTACHMENT": "{{username}} a téléversé une nouvelle pièce jointe dans {{obj_name}}", "US_CREATED": "{{username}} a créé un nouveau récit utilisateur {{obj_name}} dans {{project_name}}", - "ISSUE_CREATED": "{{username}} a créé un nouveau suivi de problème {{obj_name}} dans {{project_name}}", + "ISSUE_CREATED": "{{username}} a créé un nouveau ticket {{obj_name}} dans {{project_name}}", "TASK_CREATED": "{{username}} a créé une nouvelle Tâche {{obj_name}} dans {{project_name}}", "TASK_CREATED_WITH_US": "{{username}} a créé une nouvelle tâche {{obj_name}} dans le projet {{project_name}} pour le récit utilisateur {{us_name}}", "WIKI_CREATED": "{{username}} a créé une nouvelle page wiki {{obj_name}} dans {{project_name}}", @@ -1234,18 +1321,91 @@ "NEW_PROJECT": "{{username}} a créé le projet {{project_name}}", "MILESTONE_UPDATED": "{{username}} a mis à jour le sprint {{obj_name}}", "US_UPDATED": "{{username}} a mis à jour l'attribut «{{field_name}}» du récit utilisateur {{obj_name}}", - "ISSUE_UPDATED": "{{username}} a mis à jour l'attribut «{{field_name}}» du suivi de problème {{obj_name}}", - "TASK_UPDATED": "{{username}} a mis à jour l'attribut «{{field_name}}» de la tâche {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} a mis l'attribut \"{{field_name}}\" à {{new_value}} pour le récit utilisateur {{obj_name}}", + "US_UPDATED_POINTS": "{{username}} has updated '{{role_name}}' points of the US {{obj_name}} to {{new_value}}", + "ISSUE_UPDATED": "{{username}} a mis à jour l'attribut «{{field_name}}» du ticket {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} a mis l'attribut \"{{field_name}}\" à {{new_value}} pour le ticket {{obj_name}}", + "TASK_UPDATED": "{{username}} a mis l'attribut \"{{field_name}}\" à {{new_value}} pour la tâche {{obj_name}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} a mis l'attribut \"{{field_name}}\" à {{new_value}} pour la tâche {{obj_name}}", "TASK_UPDATED_WITH_US": "{{username}} a mis à jour l'attribut «{{field_name}}» de la tâche {{obj_name}} qui appartient au récit utilisateur {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} a mis l'attribut \"{{field_name}}\" à {{new_value}} pour la tâche {{obj_name}} appartenant au récit utilisateur {{us_name}}", "WIKI_UPDATED": "{{username}} a mis à jour la page wiki {{obj_name}}", "NEW_COMMENT_US": "{{username}} a commenté le récit utilisateur {{obj_name}}", - "NEW_COMMENT_ISSUE": "{{username}} a commenté le suivi de problème {{obj_name}}", + "NEW_COMMENT_ISSUE": "{{username}} a commenté le ticket {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} a commenté la tâche {{obj_name}}", "NEW_MEMBER": "{{project_name}} a un nouveau membre", "US_ADDED_MILESTONE": "{{username}} a ajouté le récit utilisateur {{obj_name}} à {{sprint_name}}", + "US_MOVED": "{{username}} has moved the US {{obj_name}}", "US_REMOVED_FROM_MILESTONE": "{{username}} a ajouté le récit utilisateur {{obj_name}} au carnet", "BLOCKED": "{{username}} a bloqué {{obj_name}}", "UNBLOCKED": "{{username}} a débloqué {{obj_name}}", "NEW_USER": "{{username}} a rejoint Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "An external app requires authentication", + "PAGE_DESCRIPTION": "An external app requires authentication", + "AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?", + "LOGIN_WITH_ANOTHER_USER": "Login with another user", + "AUTHORIZE_APP": "Autoriser l'application", + "CANCEL": "Annuler" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Votre projet", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Projets en cours", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Observant", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "Récits utilisateur", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } } } \ No newline at end of file diff --git a/app/locales/locale-it.json b/app/locales/locale-it.json new file mode 100644 index 00000000..8c4e02b4 --- /dev/null +++ b/app/locales/locale-it.json @@ -0,0 +1,1411 @@ +{ + "COMMON": { + "YES": "Sì", + "NO": "No", + "LOADING": "Caricamento...", + "LOADING_PROJECT": "Stiamo caricando il progetto...", + "DATE": "DD MMM YYYY", + "DATETIME": "DD MMM YYYY HH:mm", + "SAVE": "Salva", + "CANCEL": "Annulla", + "ACCEPT": "Accetta", + "DELETE": "Elimina", + "CREATE": "Crea", + "ADD": "Aggiungi", + "COPY_TO_CLIPBOARD": "Copia nella clipboard: Ctrl+C", + "EDIT": "Modifica", + "DRAG": "Drag", + "TAG_LINE": "Il tuo agile, libero e opensource strumento di project management", + "TAG_LINE_2": "ADORO IL VOSTRO PROGETTO", + "BLOCK": "Blocca", + "UNBLOCK": "Sblocca", + "BLOCKED": "Bloccato", + "CREATED_BY": "Creato da {{fullDisplayName}}", + "FROM": "da", + "TO": "a", + "CLOSE": "chiudi", + "BLOCKED_NOTE": "Perchè questa storia è bloccata?", + "BLOCKED_REASON": "Spiega il motivo", + "GO_HOME": "Ritorna all'inizio", + "PLUGINS": "Plugin", + "BETA": "Siamo in beta!", + "ONE_ITEM_LINE": "Un elemento per riga...", + "NEW_BULK": "Nuovo inserimento nel carico", + "RELATED_TASKS": "Compiti correlati", + "LOGOUT": "Esci", + "EXTERNAL_USER": "un utente esterno", + "GENERIC_ERROR": "C'é uno dei nostri Digital Champions che dice {{error}}.", + "IOCAINE_TEXT": "Sei stremato? Assicurati che gli altri lo sappiano cliccando su 'aspirina' quando stai lavorando su un compito che ti affatica. Ma non demordere, ricordati: si può migliorare solo se di tanto in tanto si accettano sfide extra!", + "CAPSLOCK_WARNING": "Attento! stai scrivendo in maiuscolo e questo input è sensibile alle maiuscole..", + "FORM_ERRORS": { + "DEFAULT_MESSAGE": "Questo valore non è valido.", + "TYPE_EMAIL": "Questo valore dovrebbe corrispondere ad una mail valida", + "TYPE_URL": "Questo valore dovrebbe corrispondere ad un URL valido", + "TYPE_URLSTRICT": "Questo valore dovrebbe corrispondere ad un URL valido", + "TYPE_NUMBER": "Questo valore dovrebbe corrispondere ad un numero valido", + "TYPE_DIGITS": "Questo valore dovrebbe essere in cifre", + "TYPE_DATEISO": "Questo valore dovrebbe corrispondere ad una data valida (YYYY-MM-DD).", + "TYPE_ALPHANUM": "Questo valore dovrebbe corrispondere ad un alfanumerico", + "TYPE_PHONE": "Questo valore dovrebbe corrispondere ad un numero telefonico valido", + "NOTNULL": "Questo valore non può essere null.", + "NOT_BLANK": "Questo valore non dovrebbe essere vuoto", + "REQUIRED": "Questo valore è richiesto", + "REGEXP": "Questo valore non è valido.", + "MIN": "Questo valore dovrebbe essere maggiore o uguale a %s.", + "MAX": "Questo valore dovrebbe essere minore o uguale a %s.", + "RANGE": "Questo valore dovrebbe essere tra %s e %s", + "MIN_LENGTH": "Questo valore è troppo corto. Dovrebbe corrispondere a %s o più caratteri.", + "MAX_LENGTH": "Questo valore è troppo lungo. Dovrebbe corrispondere a %s o meno caratteri.", + "RANGE_LENGTH": "La lunghezza del valore non è valida. Dovrebbe essere tra %s e %s caratteri.", + "MIN_CHECK": "Devi selezionare almeno %s scelte.", + "MAX_CHECK": "Devi selezionare la scelta %s o meno.", + "RANGE_CHECK": "Devi selezionare tra %s e %s scelta.", + "EQUAL_TO": "Il valore dovrebbe essere uguale" + }, + "PICKERDATE": { + "FORMAT": "DD MMM YYYY", + "IS_RTL": "falso", + "FIRST_DAY_OF_WEEK": "1", + "PREV_MONTH": "Mese precedente", + "NEXT_MONTH": "Prossimo mese", + "MONTHS": { + "JAN": "Gennaio", + "FEB": "Febbraio", + "MAR": "Marzo", + "APR": "Aprile", + "MAY": "Maggio", + "JUN": "Giugno", + "JUL": "Luglio", + "AUG": "Agosto", + "SEP": "Settembre", + "OCT": "Ottobre", + "NOV": "Novembre", + "DEC": "Dicembre" + }, + "WEEK_DAYS": { + "SUN": "Domenica", + "MON": "Lunedì", + "TUE": "Martedì", + "WED": "Mercoledì", + "THU": "Giovedì", + "FRI": "Venerdì", + "SAT": "Sabato" + }, + "WEEK_DAYS_SHORT": { + "SUN": "Dom", + "MON": "Lun", + "TUE": "Mar", + "WED": "Mer", + "THU": "Gio", + "FRI": "Ven", + "SAT": "Sat" + } + }, + "SEE_USER_PROFILE": "Guarda il profilo di {{username }}", + "USER_STORY": "Storia utente", + "TASK": "Compito", + "ISSUE": "Problema", + "TAGS": { + "PLACEHOLDER": "Eccomi! taggami", + "DELETE": "Elimina tag", + "ADD": "Aggiungi un tag" + }, + "DESCRIPTION": { + "EMPTY": "Gli spazi vuoti sono così noiosi...dai, inserisci qualche descrizione...", + "NO_DESCRIPTION": "Ancora nessuna descrizione disponibile" + }, + "FIELDS": { + "SUBJECT": "Oggetto", + "NAME": "Nome", + "URL": "URL", + "DESCRIPTION": "Descrizione", + "VALUE": "Valore", + "SLUG": "Lumaca", + "COLOR": "Colore", + "IS_CLOSED": "È chiuso?", + "STATUS": "Stato", + "TYPE": "Tipo", + "SEVERITY": "Gravità", + "PRIORITY": "Priorità", + "ASSIGNED_TO": "Assegnato a", + "POINTS": "Punti", + "BLOCKED_NOTE": "nota bloccata", + "IS_BLOCKED": "è bloccato", + "REF": "Riferimento", + "VOTES": "Voti" + }, + "ROLES": { + "ALL": "Tutti" + }, + "ASSIGNED_TO": { + "NOT_ASSIGNED": "Non assegnato", + "DELETE_ASSIGNMENT": "Elimina incarico", + "REMOVE_ASSIGNED": "Rimuovi incaricato", + "TOO_MANY": "...troppi utenti, continua a filtrare", + "CONFIRM_UNASSIGNED": "Sei sicuro di volerlo lasciare non assegnato?", + "TITLE_ACTION_EDIT_ASSIGNMENT": "Modifica incarico" + }, + "STATUS": { + "CLOSED": "Chiuso", + "OPEN": "Apri" + }, + "WATCHERS": { + "ADD": "Aggiungi osservatori", + "TITLE_ADD": "Aggiungi un membro di progetto alla lista degli osservatori", + "DELETE": "Elimina osservatore", + "TITLE_LIGHTBOX_DELETE_WARTCHER": "Elimina osservatore..." + }, + "WATCH_BUTTON": { + "WATCH": "Osserva", + "WATCHING": "In osservazione", + "UNWATCH": "Non osservare", + "WATCHERS": "Osservatori", + "BUTTON_TITLE": "Osserva/Non osservare questo elemento", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Upvoted", + "DOWNVOTE": "Downvote", + "VOTERS": "Votanti", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, + "CUSTOM_ATTRIBUTES": { + "CUSTOM_FIELDS": "Campi Personalizzati", + "SAVE": "Salva il campo personalizzato", + "EDIT": "Modifica Campo Personalizzato", + "DELETE": "Elimina l'attributo personalizzato", + "CONFIRM_DELETE": "Ricordati che tutti i valori nel campo personalizzato saranno cancellati. Sei sicuro di voler continuare?" + }, + "FILTERS": { + "TITLE": "filtri", + "INPUT_PLACEHOLDER": "Soggetto o referenza", + "TITLE_ACTION_FILTER_BUTTON": "cerca", + "BREADCRUMB_TITLE": "Indietro alle categorie", + "BREADCRUMB_FILTERS": "Filtri", + "BREADCRUMB_STATUS": "stato" + }, + "WYSIWYG": { + "H1_BUTTON": "Intestazione di primo livello", + "H1_SAMPLE_TEXT": "Il titolo qui...", + "H2_BUTTON": "Intestazione di secondo livello", + "H2_SAMPLE_TEXT": "Il titolo qui...", + "H3_BUTTON": "Intestazione die terzo livello", + "H3_SAMPLE_TEXT": "Il titolo qui...", + "BOLD_BUTTON": "Grassetto", + "BOLD_BUTTON_SAMPLE_TEXT": "Inserire qui il testo...", + "ITALIC_BUTTON": "Italico", + "ITALIC_SAMPLE_TEXT": "Inserire qui il testo...", + "STRIKE_BUTTON": "Centro", + "STRIKE_SAMPLE_TEXT": "Inserire qui il testo...", + "BULLETED_LIST_BUTTON": "Elenco puntato", + "BULLETED_LIST_SAMPLE_TEXT": "Inserire qui il testo...", + "NUMERIC_LIST_BUTTON": "Elenco numerato", + "NUMERIC_LIST_SAMPLE_TEXT": "Inserire qui il testo...", + "PICTURE_BUTTON": "Foto", + "PICTURE_SAMPLE_TEXT": "Il testo per la tua foto qui...", + "LINK_BUTTON": "Link", + "LINK_SAMPLE_TEXT": "Qui il testo da linkare...", + "QUOTE_BLOCK_BUTTON": "Citazione", + "QUOTE_BLOCK_SAMPLE_TEXT": "Inserire qui il testo...", + "CODE_BLOCK_BUTTON": "Blocco di codice", + "CODE_BLOCK_SAMPLE_TEXT": "Inserire qui il testo...", + "PREVIEW_BUTTON": "Anteprima", + "EDIT_BUTTON": "Modifica", + "MARKDOWN_HELP": "Aiuto per la sintassi Markdown" + }, + "PERMISIONS_CATEGORIES": { + "SPRINTS": { + "NAME": "Sprints", + "VIEW_SPRINTS": "Vedi gli sprint", + "ADD_SPRINTS": "Aggiungi gli sprints", + "MODIFY_SPRINTS": "Modifica gli sprint", + "DELETE_SPRINTS": "Elimina sprint" + }, + "USER_STORIES": { + "NAME": "Storie Utente", + "VIEW_USER_STORIES": "Vai alle storie utente", + "ADD_USER_STORIES": "Aggiungi le storie utente", + "MODIFY_USER_STORIES": "Modifica le storie utente", + "DELETE_USER_STORIES": "Elimina le storie utente" + }, + "TASKS": { + "NAME": "Compiti", + "VIEW_TASKS": "Guarda i compiti", + "ADD_TASKS": "Aggiungi i compiti", + "MODIFY_TASKS": "Modifica i compiti", + "DELETE_TASKS": "Elimina i compiti" + }, + "ISSUES": { + "NAME": "problemi", + "VIEW_ISSUES": "Guarda i problemi", + "ADD_ISSUES": "Aggiungi un problema", + "MODIFY_ISSUES": "Modifica i problemi", + "DELETE_ISSUES": "Elimina i problemi" + }, + "WIKI": { + "NAME": "Wiki", + "VIEW_WIKI_PAGES": "Guarda le pagine wiki", + "ADD_WIKI_PAGES": "Aggiungi le pagine wiki", + "MODIFY_WIKI_PAGES": "Modifica le pagine wiki", + "DELETE_WIKI_PAGES": "Elimina le pagine wiki", + "VIEW_WIKI_LINKS": "Vedi il link di wiki", + "ADD_WIKI_LINKS": "Aggiungi un wiki links", + "DELETE_WIKI_LINKS": "Elimina il link wiki" + } + }, + "META": { + "PAGE_TITLE": "Taiga", + "PAGE_DESCRIPTION": "Taiga è una piattaforma di gestione dei progetti per startups e sviluppatori agile, o per designers che vogliono uno strumento semplice ed elegante che renda piacevole lavorare. " + } + }, + "LOGIN": { + "PAGE_TITLE": "Accesso - Taiga", + "PAGE_DESCRIPTION": "Accedi a Taiga, una piattaforma semplice di gestione dei progetti che rende il lavoro davvero piacevole alle start-up e a chi sviluppa e progetta con il metodo \"agile\"." + }, + "AUTH": { + "INVITED_YOU": "ti ha invitato a partecipare al progetto", + "NOT_REGISTERED_YET": "Non sei ancora registrato?", + "REGISTER": "Registrati", + "CREATE_ACCOUNT": "Crea il tuo account gratuito qui" + }, + "LOGIN_COMMON": { + "HEADER": "Ho già un account Taiga", + "PLACEHOLDER_AUTH_NAME": "nome utente o email (occhio alle maiuscole)", + "LINK_FORGOT_PASSWORD": "Password dimenticata?", + "TITLE_LINK_FORGOT_PASSWORD": "Password dimenticata?", + "ACTION_ENTER": "Conferma", + "ACTION_SIGN_IN": "Accedi", + "PLACEHOLDER_AUTH_PASSWORD": "Password (occhio alle maiuscole)" + }, + "LOGIN_FORM": { + "ERROR_AUTH_INCORRECT": "Secondo i nostri Digital Champions, i tuoi nomi utente, email o password sono errati.", + "SUCCESS": "I nostri Digital Champions sono entusiasti! Benvenuto in Taiga!" + }, + "REGISTER": { + "PAGE_TITLE": "Registrazione - Taiga", + "PAGE_DESCRIPTION": "Crea il tuo account su Taiga, una piattaforma semplice di project management che rende il lavoro davvero piacevole alle start-up e a chi sviluppa e progetta con il metodo \"agile\"." + }, + "REGISTER_FORM": { + "TITLE": "Registra un nuovo account Taiga (è gratis)", + "PLACEHOLDER_NAME": "Scegli uno nome utente (occhio alle maiuscole)", + "PLACEHOLDER_FULL_NAME": "Inserisci il tuo nome completo", + "PLACEHOLDER_EMAIL": "La tua email", + "PLACEHOLDER_PASSWORD": "Imposta una password (case sensitive)", + "ACTION_SIGN_UP": "Registrati", + "TITLE_LINK_LOGIN": "Log in", + "LINK_LOGIN": "Sei già registrato? Effettua l'accesso" + }, + "FORGOT_PASSWORD": { + "PAGE_TITLE": "Password dimenticata - Taiga", + "PAGE_DESCRIPTION": "Inserisci il tuo nome utente o la tua email per ricevere una nuova password a poter accedere nuovamente a Taiga." + }, + "FORGOT_PASSWORD_FORM": { + "TITLE": "Oops, hai dimenticato la password?", + "SUBTITLE": "Inserisci il tuo nome utente o email per ricevere una nuova password", + "PLACEHOLDER_FIELD": "Nome utente o email", + "ACTION_RESET_PASSWORD": "Reimposta la password", + "LINK_CANCEL": "Fa niente dai, riportami indietro. Penso di ricordarmela.", + "SUCCESS": "Controlla la tua posta!
Ti abbiamo inviato una email con le istruzioni per reimpostare la password", + "ERROR": "Secondo i nostri Digital Champions, non sei ancora registrato." + }, + "CHANGE_PASSWORD": { + "PAGE_TITLE": "Cambia la tua password - Taiga", + "PAGE_DESCRIPTION": "Imposta una nuova password per il tuo account Taiga... hey! ti consigliamo di mangiare cibo ricco di ferro, fa bene all'innovazione :P", + "SECTION_NAME": "Cambia password", + "FIELD_CURRENT_PASSWORD": "Password attuale", + "PLACEHOLDER_CURRENT_PASSWORD": "La tua password corrente (o lascia vuoto se non hai ancora una password)", + "FIELD_NEW_PASSWORD": "Nuova password", + "PLACEHOLDER_NEW_PASSWORD": "Digita una nuova password", + "FIELD_RETYPE_PASSWORD": "Digita una nuova password", + "PLACEHOLDER_RETYPE_PASSWORD": "Ri-digita una nuova password", + "ERROR_PASSWORD_MATCH": "Le password non corrispondono" + }, + "CHANGE_PASSWORD_RECOVERY_FORM": { + "TITLE": "Crea una nuova password su Taiga", + "SUBTITLE": "Ehi, e se mangiassi qualcosa ricco di ferro? fa bene all'innovazione :p", + "PLACEHOLDER_NEW_PASSWORD": "Nuova password", + "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Digita una nuova password", + "ACTION_RESET_PASSWORD": "Reimposta la password", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", + "SUCCESS": "I nostri Digital Champions hanno salvato la tua nuova password. Che bravi!
Pova a fare login con questa." + }, + "INVITATION": { + "PAGE_TITLE": "Invito ad accedere - Taiga", + "PAGE_DESCRIPTION": "Accetta l'invito a partecipare ad un progetto in Taiga, una piattaforma semplice di project management che rende il lavoro davvero piacevole a start-up e a chi sviluppa e progetta con il metodo \"agile\"." + }, + "INVITATION_LOGIN_FORM": { + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", + "SUCCESS": "Sei ora un membro di questo progetto. Benvenuto in {{project_name}}", + "ERROR": "Secondo i nostri Digital Champions, non sei ancora regisrtrato o hai inserito una password non valida." + }, + "HOME": { + "PAGE_TITLE": "Home - Taiga", + "PAGE_DESCRIPTION": "La home di Taiga con i tuoi principali progetti e tutti ile storie utente, compiti e problemi assegnati e osservati. ", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", + "EMPTY_PROJECT_LIST": "Per ora non hai nessun progetto", + "WORKING_ON_SECTION": "Sta lavorando su", + "WATCHING_SECTION": "Osservando" + }, + "PROJECTS": { + "PAGE_TITLE": "I miei progetti - Taiga", + "PAGE_DESCRIPTION": "Una lista di tutti i tuoi progetti, la puoi riordinare o crearne una nuova.", + "MY_PROJECTS": "I miei progetti" + }, + "ATTACHMENT": { + "SECTION_NAME": "allegati", + "TITLE": "{{ fileName }} caricato il {{ date }}", + "DESCRIPTION": "Inserisci una breve descrizione", + "DEPRECATED": "(deprecato)", + "DEPRECATED_FILE": "Deprecato?", + "ADD": "Aggiungi un nuovo allegato. {{maxFileSizeMsg}}", + "MAX_FILE_SIZE": "[Dimensione max. {{maxFileSize}}]", + "SHOW_DEPRECATED": "+ visualizza allegati deprecati", + "HIDE_DEPRECATED": "- nascondi allegati deprecati", + "COUNT_DEPRECATED": "({{ counter }} deprecati)", + "MAX_UPLOAD_SIZE": "La dimensione massima di caricamento è {{maxFileSize}}", + "DATE": "DD MMM YYYY [at] hh:mm", + "ERROR_UPLOAD_ATTACHMENT": "Non é stato possibile caricare '{{fileName}}'. {{errorMessage}}", + "TITLE_LIGHTBOX_DELETE_ATTACHMENT": "Cancella l'allegato", + "MSG_LIGHTBOX_DELETE_ATTACHMENT": "l'allegato '{{fileName}}'", + "ERROR_DELETE_ATTACHMENT": "Non siamo riusciti a cancellare: {{errorMessage}}", + "FIELDS": { + "IS_DEPRECATED": "è deprecato" + } + }, + "PAGINATION": { + "PREVIOUS": "Precedente", + "NEXT": "Successivo" + }, + "ADMIN": { + "COMMON": { + "TITLE_ACTION_EDIT_VALUE": "Modifica valore", + "TITLE_ACTION_DELETE_VALUE": "Elimina valore" + }, + "HELP": "Hai bisogno di aiuto? Controlla la nostra pagina di supporto!", + "PROJECT_DEFAULT_VALUES": { + "TITLE": "Valori di default", + "SUBTITLE": "Imposta valori di default per tutti i selettori input." + }, + "MEMBERSHIPS": { + "TITLE": "Gestisci membri", + "PAGE_TITLE": "Webhooks - {{projectName}}", + "ADD_BUTTON": "Nuovo Membro", + "ADD_BUTTON_TITLE": "Aggiungi un nuovo membro" + }, + "PROJECT_EXPORT": { + "TITLE": "Esporta", + "SUBTITLE": "Esporta il tuo progetto e salva un backup o creane uno nuovo basato su questo", + "EXPORT_BUTTON": "Esporta", + "EXPORT_BUTTON_TITLE": "Esporta il tuo progetto", + "LOADING_TITLE": "Stiamo preparando il file di dump", + "DUMP_READY": "Il file di dump é pronto!", + "LOADING_MESSAGE": "Non chiudere questa pagina.", + "ASYNC_MESSAGE": "Ti invieremo una mail quando sarà pronto", + "SYNC_MESSAGE": "Se il download non parte automaticamente clicca here.", + "ERROR": "I nostri Digital Champion hanno qualche problema a generare il tuo dump. Prova di nuova.", + "ERROR_BUSY": "Scusali, i nostri Digital Champions sono occupati. Riprova di nuovo in qualche minuto.", + "ERROR_MESSAGE": "Accidenti, i nostri Digital Champions hanno qualche problema a generare il tuo dump: {{message}}" + }, + "MODULES": { + "TITLE": "Moduli", + "ENABLE": "Abilita", + "DISABLE": "Disabilita", + "BACKLOG": "Backlog", + "BACKLOG_DESCRIPTION": "Amministra le storie degli utenti per mantenere una visione organizzata dei lavori in arrivo e di quelli ad alta priorità ", + "KANBAN": "Kanban", + "KANBAN_DESCRIPTION": "Organizza in modo semplice il tuo progetto con questa board", + "ISSUES": "problemi", + "ISSUES_DESCRIPTION": "Traccia i bug, le domande e i miglioramenti legati al tuo progetto. Non perderti nulla!", + "WIKI": "Wiki", + "WIKI_DESCRIPTION": "Aggiungi, modifica o elimina i contenuti in collaborazione con gli altri. E' il posto giusto per la documentazione del tuo progetto", + "MEETUP": "Incontro", + "MEETUP_DESCRIPTION": "Scegli il tuo sistema di videoconferenza. Anche gli sviluppatori hanno bisogno di qualche contatto faccia a faccia. ", + "SELECT_VIDEOCONFERENCE": "Seleziona un sistema di videoconferenza", + "SALT_CHAT_ROOM": "Se preferisci puoi aggiungere un valore sale al nome della chat room", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Personalizzato", + "URL_CHAT_ROOM": "URL della tua chat room" + }, + "PROJECT_PROFILE": { + "PAGE_TITLE": "{{sectionName}} - Profilo progetto - {{projectName}}", + "PROJECT_DETAILS": "Dettagli progetto", + "PROJECT_NAME": "Nome progetto", + "PROJECT_SLUG": "Progetto lumaca", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", + "TAGS": "Tag", + "DESCRIPTION": "Descrizione", + "PUBLIC_PROJECT": "Progetto pubblico", + "PRIVATE_PROJECT": "Progetto privato", + "DELETE": "Elimina questo progetto" + }, + "REPORTS": { + "TITLE": "Rapporti", + "SUBTITLE": "Esporta il tuo progetto dati in formato CSV a crea il tuo report", + "DESCRIPTION": "Scarica il file CSV o copia l'url ed usa il tuo editor preferito o spreadsheet per realizzare i tuoi report. Sarai in grado di visualizzare ed analizzare i tuoi dati molto più facilmente vedrai. ", + "HELP": "Come utilizzarlo all'interno di uno spreadsheet?", + "REGENERATE_TITLE": "Cambia URL", + "REGENERATE_SUBTITLE": "Stai per modificare l'url di accesso al CSV. il precedente url verrá disabilitato. Sei sicuro?" + }, + "CSV": { + "SECTION_TITLE_US": "Report delle storie utente", + "SECTION_TITLE_TASK": "Analisi dei compiti", + "SECTION_TITLE_ISSUE": "Report criticitá", + "DOWNLOAD": "Scarica CSV", + "URL_FIELD_PLACEHOLDER": "Per piacere rigenera l'url del CSV", + "TITLE_REGENERATE_URL": "Rigenera l'url del CSV", + "ACTION_GENERATE_URL": "Genera Url", + "ACTION_REGENERATE": "Rigenera" + }, + "CUSTOM_FIELDS": { + "TITLE": "Campi Personalizzati", + "SUBTITLE": "Specifica i campi personalizzati per le tue Storie Utente, compiti e problemi", + "US_DESCRIPTION": "Campi personalizzati delle storie utente", + "US_ADD": "Aggiungi un campo personalizzato nelle storie utente", + "TASK_DESCRIPTION": "Campi personalizzati dei Compiti", + "TASK_ADD": "Aggiungi campo personalizzato nei compiti", + "ISSUE_DESCRIPTION": "Campi personalizzati", + "ISSUE_ADD": "Aggiungi un campo personalizzato alla criticitá", + "FIELD_TYPE_TEXT": "Testo", + "FIELD_TYPE_MULTI": "Multilinea", + "FIELD_TYPE_DATE": "Data" + }, + "PROJECT_VALUES": { + "PAGE_TITLE": "{{sectionName}} - Valori di progetto - {{projectName}}", + "REPLACEMENT": "Tutti gli elementi con questo valore saranno modificati con", + "ERROR_DELETE_ALL": "Non puoi cancellare tutti i valori" + }, + "PROJECT_VALUES_POINTS": { + "TITLE": "Punti", + "SUBTITLE": "Specifica i punti vostre storie utente potrebbero essere stimati a", + "US_TITLE": "Punti Storie Utente ", + "ACTION_ADD": "Aggiungi un nuovo punto" + }, + "PROJECT_VALUES_PRIORITIES": { + "TITLE": "priorità", + "SUBTITLE": "Specifica le prioritá della criticitá", + "ISSUE_TITLE": "Prioritá della criticitá", + "ACTION_ADD": "Aggiungi nuova priorità" + }, + "PROJECT_VALUES_SEVERITIES": { + "TITLE": "Severitá", + "SUBTITLE": "Specifica le severitá della criticitá", + "ISSUE_TITLE": "Severitá della criticitá", + "ACTION_ADD": "Aggiungi una nuova criticità." + }, + "PROJECT_VALUES_STATUS": { + "TITLE": "Stato", + "SUBTITLE": "Specifica lo stato delle storie utente, i compiti e i problemi saranno affrontati", + "US_TITLE": "Stato delle storie utente", + "TASK_TITLE": "Stato dei compiti", + "ISSUE_TITLE": "Stato dei problemi" + }, + "PROJECT_VALUES_TYPES": { + "TITLE": "Tipi", + "SUBTITLE": "Specifica di che tipo puó essere la criticitá", + "ISSUE_TITLE": "Tipi di criticitá", + "ACTION_ADD": "Aggiungi {{objName}}" + }, + "ROLES": { + "PAGE_TITLE": "Ruoli - {{projectName}}", + "WARNING_NO_ROLE": "Attento, nessun ruolo, all'interno del tuo progetto, potrà stimare i punti valore per le storie utente", + "HELP_ROLE_ENABLED": "Una volta abilitato, chi é associato a questo ruolo sará in grado di stimare il valore dei punti per le storie utente", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", + "COUNT_MEMBERS": "{{ role.members_count }} membri con questo ruolo", + "TITLE_DELETE_ROLE": "Elimina ruolo", + "REPLACEMENT_ROLE": "Tutti gli utenti con questo ruolo saranno spostati a ", + "WARNING_DELETE_ROLE": "Attento, tutte le stime dei ruoli saranno eliminate", + "ERROR_DELETE_ALL": "Non puoi cancellare tutti i valori", + "EXTERNAL_USER": "Utente esterno" + }, + "THIRD_PARTIES": { + "SECRET_KEY": "Chiave segreta", + "PAYLOAD_URL": "Payload URL", + "VALID_IPS": "Origina valida per l'IP (separato da ,)" + }, + "BITBUCKET": { + "SECTION_NAME": "Bitbucket", + "PAGE_TITLE": "Bitbucket - {{projectName}}", + "INFO_VERIFYING_IP": "Le richieste Bitbucket non sono firmate, il miglior modo di verificarne l'origine è tramite l'IP. Se il campo è vuoto non ci sarà nessuna validazione." + }, + "GITLAB": { + "SECTION_NAME": "Gitlab", + "PAGE_TITLE": "Gitlab - {{projectName}}", + "INFO_VERIFYING_IP": "Le richieste Gitlab non sono firmate, quindi il miglior modo di verificarle è attraverso l'origine degli IP. Se il campo è vuoto non ci sarà validazione." + }, + "GITHUB": { + "SECTION_NAME": "Github", + "PAGE_TITLE": "Github - {{projectName}}" + }, + "WEBHOOKS": { + "PAGE_TITLE": "Webhooks - {{projectName}}", + "SECTION_NAME": "Webhooks", + "SUBTITLE": "Webhooks notifica servizi esterni su eventi in Taiga, come i commenti, le storie utente....", + "ADD_NEW": "Aggiungi un nuovo Webhook", + "TYPE_NAME": "Digita il nome del servizio", + "TYPE_PAYLOAD_URL": "Digita l'URL del servizio in carico", + "TYPE_SERVICE_SECRET": "Inserisci la chiave segreta di servizio", + "SAVE": "Salva Webhook", + "CANCEL": "Elimina Webhook", + "SHOW_HISTORY": "(Mostra la storia)", + "TEST": "Test del Webhook", + "EDIT": "Modifica Webhook", + "DELETE": "Elimina Webhook", + "REQUEST": "Richiesta", + "RESEND_REQUEST": "Ri-inoltra la richiesta", + "HEADERS": "Intestazioni", + "PAYLOAD": "Carico", + "RESPONSE": "Risposta", + "DATE": "DD MMM YYYY [at] hh:mm:ss", + "ACTION_HIDE_HISTORY": "(Nascondi la storia)", + "ACTION_HIDE_HISTORY_TITLE": "Nascondi i dettagli della storia", + "ACTION_SHOW_HISTORY": "(Mostra la storia)", + "ACTION_SHOW_HISTORY_TITLE": "Mostra i dettagli della storia", + "WEBHOOK_NAME": "Webhook '{{name}}'" + }, + "CUSTOM_ATTRIBUTES": { + "PAGE_TITLE": "{{sectionName}} - Attributi personalizzati - {{projectName}}", + "ADD": "Aggiungi campo personalizzato", + "EDIT": "Modifica Campo Personalizzato", + "DELETE": "Elimina Campo Personalizzato", + "SAVE_TITLE": "Salva il campo personalizzato", + "CANCEL_TITLE": "Elimina la creazione", + "SET_FIELD_NAME": "Imposta il nome del campo predefinito", + "SET_FIELD_DESCRIPTION": "Imposta la descrizione del campo personalizzato ", + "ACTION_UPDATE": "Aggiorna il campo personalizzato", + "ACTION_CANCEL_EDITION": "Elimina versione" + }, + "MEMBERSHIP": { + "COLUMN_MEMBER": "Membro", + "COLUMN_ADMIN": "Amministratore", + "COLUMN_ROLE": "Ruolo", + "COLUMN_STATUS": "Stato", + "STATUS_ACTIVE": "Attivo", + "STATUS_PENDING": "In sospeso", + "DELETE_MEMBER": "Elimina membro", + "RESEND": "Resend", + "SUCCESS_SEND_INVITATION": "Abbiamo mandato nuovamente l'invito a '{{email}}'.", + "ERROR_SEND_INVITATION": "Non abbiamo mandato l'invito", + "SUCCESS_DELETE": "Abbiamo eliminato {{message}}.", + "ERROR_DELETE": "Non siamo riusciti ad eliminare {{message}}.", + "DEFAULT_DELETE_MESSAGE": "L'invito a {{email}}" + }, + "DEFAULT_VALUES": { + "LABEL_POINTS": "Valore standard per punti di selezione", + "LABEL_US": "Valore predefinito per la selezione di stati delle storie utente", + "LABEL_TASK_STATUS": "Valore predefinito per la selezione degli stati del compito", + "LABEL_PRIORITY": "Valore predefinito per la selezione prioritaria", + "LABEL_SEVERITY": "Valore predefinito per la selezione di rigore", + "LABEL_ISSUE_TYPE": "Valore predefinito per il tipo di selezione del problema", + "LABEL_ISSUE_STATUS": "Valore predefinito per la selezione di stato del problema" + }, + "STATUS": { + "PLACEHOLDER_WRITE_STATUS_NAME": "Scrivi un nome per il nuovo status" + }, + "TYPES": { + "PLACEHOLDER_WRITE_NAME": "Dai un nome al nuovo elemento" + }, + "US_STATUS": { + "ACTION_ADD_STATUS": "Aggiungi un nuovo status", + "IS_ARCHIVED_COLUMN": "È archiviato?", + "WIP_LIMIT_COLUMN": "Limite WIP", + "PLACEHOLDER_WRITE_NAME": "Scrivi un nome per il nuovo status" + }, + "MENU": { + "TITLE": "Amministratore", + "PROJECT": "Progetto", + "ATTRIBUTES": "Attributi", + "MEMBERS": "Membri", + "PERMISSIONS": "Permessi", + "INTEGRATIONS": "Integrazioni", + "PLUGINS": "Plugin" + }, + "SUBMENU_PROJECT_ATTRIBUTES": { + "TITLE": "Attributi" + }, + "SUBMENU_PROJECT_VALUES": { + "STATUS": "Stato", + "POINTS": "Punti", + "PRIORITIES": "priorità", + "SEVERITIES": "Severitá", + "TYPES": "Tipi", + "CUSTOM_FIELDS": "Campi personalizzati" + }, + "SUBMENU_PROJECT_PROFILE": { + "TITLE": "Profilo progetto" + }, + "SUBMENU_ROLES": { + "TITLE": "Ruoli", + "ACTION_NEW_ROLE": "+ Nuovo ruolo", + "TITLE_ACTION_NEW_ROLE": "Aggiungi nuovo ruolo" + }, + "SUBMENU_THIDPARTIES": { + "TITLE": "Servizi" + } + }, + "USER": { + "PROFILE": { + "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", + "EDIT": "Modifica profilo", + "FOLLOW": "Segui", + "CLOSED_US": "US chiusa", + "PROJECTS": "Progetti", + "PROJECTS_EMPTY": "{{username}} non ha ancora nessun progetto", + "CONTACTS": "Contatti", + "CONTACTS_EMPTY": "{{username}} non ha ancora nessun contatto", + "CURRENT_USER_CONTACTS_EMPTY": "Non hai ancora contatti", + "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Le persone con cui lavori in Taiga saranno automaticamente tra tuoi contatti", + "REPORT": "Segnala abuso", + "TABS": { + "ACTIVITY_TAB": "Attività", + "ACTIVITY_TAB_TITLE": "Mostra tutte le attività dell'utente", + "PROJECTS_TAB": "Progetti", + "PROJECTS_TAB_TITLE": "Mostra tutti i progetti di cui questo utente è membro", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Voti", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Osservato", + "WATCHED_TAB_TITLE": "Mostra tutti gli elementi osservati dall'utente", + "CONTACTS_TAB": "Contatti", + "CONTACTS_TAB_TITLE": "Lista tutti i contatti fatti dall'utente" + } + }, + "PROFILE_SIDEBAR": { + "TITLE": "Il tuo profilo", + "DESCRIPTION": "Le persone possono vedere tutto quello che fai e su cosa stai lavorando. Aggiungi una bella bio per migliorare la descrizione delle tue informazioni.", + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Inserisci testo...", + "FILTER_TYPE_ALL": "Tutti", + "FILTER_TYPE_ALL_TITLE": "Mostra tutto", + "FILTER_TYPE_PROJECTS": "Progetti", + "FILTER_TYPE_PROJECT_TITLES": "Mostra solo i progetti", + "FILTER_TYPE_USER_STORIES": "Resoconti", + "FILTER_TYPE_USER_STORIES_TITLES": "Mostra solo resoconti utente", + "FILTER_TYPE_TASKS": "Compiti", + "FILTER_TYPE_TASK_TITLES": "Mostra solo attività", + "FILTER_TYPE_ISSUES": "problemi", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." + } + }, + "PROJECT": { + "PAGE_TITLE": "TEAM - {{projectName}}", + "WELCOME": "Benvenuto", + "SECTION_PROJECTS": "Progetti", + "HELP": "Riordina i tuoi progetti per avere in alto i più usati.
I primi 10 progetti compariranno nella lista dei progetti della barra di navigazione, su in alto", + "PRIVATE": "Progetto privato", + "STATS": { + "PROJECT": "progetto
punti", + "DEFINED": "Definiti
punti", + "ASSIGNED": "assegnati
punti", + "CLOSED": "chiusi
punti" + }, + "SECTION": { + "SEARCH": "Cerca", + "TIMELINE": "Cronologia", + "BACKLOG": "Backlog", + "KANBAN": "Kanban", + "ISSUES": "problemi", + "WIKI": "Wiki", + "TEAM": "Squadra", + "MEETUP": "Incontro", + "ADMIN": "Amministratore" + }, + "NAVIGATION": { + "SECTION_TITLE": "Tuoi progetti", + "PLACEHOLDER_SEARCH": "Cerca in...", + "ACTION_CREATE_PROJECT": "Crea progetto", + "ACTION_IMPORT_PROJECT": "Importa progetto", + "SEE_MORE_PROJECTS": "Guarda più progetti", + "TITLE_CREATE_PROJECT": "Crea progetto", + "TITLE_IMPORT_PROJECT": "Importa progetto", + "TITLE_PRVIOUS_PROJECT": "Mostra i progetti precedenti", + "TITLE_NEXT_PROJECT": "Progetto successivo", + "HELP_TITLE": "Pagina di supporto Taiga", + "HELP": "Aiuto", + "FEEDBACK_TITLE": "Invia feedback", + "FEEDBACK": "Feedback", + "NOTIFICATIONS_TITLE": "Modifica le impostazioni delle notifiche", + "NOTIFICATIONS": "Notifiche", + "ORGANIZATIONS_TITLE": "Modifica la tua organizzazione", + "ORGANIZATIONS": "Modifica organizzazioni", + "SETTINGS_TITLE": "Modifica le tue impostazioni", + "SETTINGS": "Impostazioni", + "VIEW_PROFILE_TITLE": "Mostra profilo", + "VIEW_PROFILE": "Mostra profilo", + "EDIT_PROFILE_TITLE": "Modifica il tuo profilo", + "EDIT_PROFILE": "Modifica profilo", + "CHANGE_PASSWORD_TITLE": "Cambia password", + "CHANGE_PASSWORD": "Cambia password", + "DASHBOARD_TITLE": "Dashboard", + "DISCOVER_TITLE": "Scopri i progetti più seguiti", + "DISCOVER": "Scopri", + "ACTION_REORDER": "Usa drag & drop per riordinare" + }, + "IMPORT": { + "TITLE": "Importazione Progetto", + "UPLOADING_FILE": "Carico il file di dump", + "DESCRIPTION": "Questo processo puó durare un po', nel frattempo lasciare questa finestra aperta.", + "ASYNC_IN_PROGRESS_TITLE": "I nostri Digital Champions stanno lavorando per importare il tuo progetto!", + "ASYNC_IN_PROGRESS_MESSAGE": "Questo processo puó durare minuti.
Verrá inviata una mail al suo completamento", + "UPLOAD_IN_PROGRESS_MESSAGE": "Caricati {{uploadedSize}} di {{totalSize}}", + "ERROR": "Mannaggia, i Digital Champions hanno qualche problema ad importare il dump. Prova di nuovo.", + "ERROR_TOO_MANY_REQUEST": "Scusaci, i nostri Digital Champions sono di nuovo occupati. Riprova di nuovo in qualche minuto.", + "ERROR_MESSAGE": "I nostri Digital Champions hanno qualche problema ad importare il dump dei tuoi dati: {{error_message}}", + "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) è troppo pesante per i nostri Digital Champions; falli contenti, prova con una dimensione minore di ({{maxFileSize}})", + "SYNC_SUCCESS": "Il tuo progetto è stato importato con successo" + }, + "LIKE_BUTTON": { + "LIKE": "Mi piace", + "LIKED": "Liked", + "UNLIKE": "Non mi piace", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Osserva questo progetto ed imposta la politica di notifica", + "WATCH": "Osserva", + "WATCHING": "In osservazione", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Ricevi tutte le notifiche", + "NOTIFY_ALL_TITLE": "Ricevi tutte le notifiche per questo progetto", + "NOTIFY_INVOLVED": "Solo se conivolti", + "NOTIFY_INVOLVED_TITLE": "Ricevi notifiche solo quando coinvolto", + "UNWATCH": "Non osservare", + "UNWATCH_TITLE": "Non osservare il progetto" + } + } + }, + "LIGHTBOX": { + "DELETE_ACCOUNT": { + "SECTION_NAME": "Elimina Account Taiga", + "CONFIRM": "Sei sicuro di voler eliminare il tuo account Taiga?", + "SUBTITLE": "Ci mancherai! :-(", + "NEWSLETTER_LABEL_TEXT": "Non voglio più ricevere le vostre newsletter" + }, + "DELETE_PROJECT": { + "TITLE": "Elimina progetto", + "QUESTION": "Sei sicuro di voler cancellare questo progetto?", + "SUBTITLE": "Tutti i dati di progetto (user story, compiti, temi, sprint e pagine wiki) andranno persi :-(", + "CONFIRM": "Si, sono proprio sicuro" + }, + "ASSIGNED_TO": { + "SELECT": "Selezione assegnata a ", + "SEARCH": "Cerca per utenti" + }, + "ADD_MEMBER": { + "TITLE": "Nuovo Membro", + "HELP_TEXT": "Se gli utenti sono già registrati su Taiga, verranno aggiunti automaticamente. In caso contrario, riceveranno un invito." + }, + "CREATE_ISSUE": { + "TITLE": "Aggiungi problema" + }, + "FEEDBACK": { + "TITLE": "Raccontaci qualcosa...", + "COMMENT": "...un bug, dei suggerimenti, qualcosa di fico... o anche il tuo peggior incubo con Taiga", + "ACTION_SEND": "Invia feedback" + }, + "SEARCH": { + "TITLE": "Cerca", + "PLACEHOLDER_SEARCH": "Cosa stai cercando?" + }, + "ADD_EDIT_SPRINT": { + "TITLE": "Nuovo sprint", + "PLACEHOLDER_SPRINT_NAME": "il nome dello sprint", + "PLACEHOLDER_SPRINT_START": "Inizio stimato", + "PLACEHOLDER_SPRINT_END": "Fine stimata", + "ACTION_DELETE_SPRINT": "Sei sicuro di voler cancellare questo sprint?", + "TITLE_ACTION_DELETE_SPRINT": "Elimina sprint", + "LAST_SPRINT_NAME": "l'ultimo sprint è {{lastSprint}} ;-) " + }, + "CREATE_EDIT_TASK": { + "TITLE": "Nuovo compito", + "PLACEHOLDER_SUBJECT": "Soggetto del compito", + "PLACEHOLDER_STATUS": "Situazione del compito", + "OPTION_UNASSIGNED": "Non assegnato", + "PLACEHOLDER_SHORT_DESCRIPTION": "Inserisci una descrizione breve", + "ACTION_EDIT": "Modifica compito" + }, + "CREATE_EDIT_US": { + "TITLE": "Nuova storia utente", + "PLACEHOLDER_DESCRIPTION": "Aggiungere un testo descrittivo che aiuti gli altri nel comprendere questa storia utente", + "NEW_US": "Nuova storia utente", + "EDIT_US": "Modifica la storia utente" + }, + "DELETE_SPRINT": { + "TITLE": "Cancella sprint" + }, + "CREATE_MEMBER": { + "PLACEHOLDER_INVITATION_TEXT": "(facoltativo) aggiungi un testo personalizzato all'invito. Di qualcosa di simpatico ai tuoi nuovi membri ;-)", + "PLACEHOLDER_TYPE_EMAIL": "Scrivi una mail" + } + }, + "US": { + "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completata per il {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} di {{userStoryTotalTasks}} tasks closed). Punti: {{userStoryPoints}}. Descrizione: {{userStoryDescription}}", + "SECTION_NAME": "Dettagli della storia utente", + "LINK_TASKBOARD": "Pannello dei compiti", + "TITLE_LINK_TASKBOARD": "Vai al pannello dei compiti", + "TOTAL_POINTS": "totale punti", + "ADD": "+ Aggiungi una nuova storia utente", + "ADD_BULK": "Aggiungere qualche nuova User Storie al carico", + "PROMOTED": "Questa storia utente è stata promossa da problema:", + "TITLE_LINK_GO_TO_ISSUE": "Vai al problema", + "EXTERNAL_REFERENCE": "Questo US é stato creato da", + "GO_TO_EXTERNAL_REFERENCE": "Ritorna all'inizio", + "BLOCKED": "Questa storia utente è bloccata", + "PREVIOUS": "Storia utente precedente ", + "NEXT": "Prossima storia utente", + "TITLE_DELETE_ACTION": "Elimina la storia utente", + "LIGHTBOX_TITLE_BLOKING_US": "Blocco la storia utente", + "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} compiti completati", + "ASSIGN": "Assegna la storia utente", + "NOT_ESTIMATED": "Non stimato", + "TOTAL_US_POINTS": "Totale punti della storia utente", + "FIELDS": { + "TEAM_REQUIREMENT": "Requisito del team", + "CLIENT_REQUIREMENT": "Requisito del client", + "FINISH_DATE": "Data di conclusione" + } + }, + "COMMENTS": { + "DELETED_INFO": "Commento cancellato da {{user}} il {{date}}", + "TITLE": "Commenti", + "COMMENT": "Commento", + "TYPE_NEW_COMMENT": "Scrivi un nuovo commento qui", + "SHOW_DELETED": "Visualizza commento cancellato", + "HIDE_DELETED": "Nascondi commento cancellato", + "RESTORE": "Ripristina commento" + }, + "ACTIVITY": { + "SHOW_ACTIVITY": "Mostra attività", + "DATETIME": "DD MMM YYYY HH:mm", + "SHOW_MORE": "Mostra gli inserimenti precedenti ({{showMore}} more)", + "TITLE": "Attività", + "REMOVED": "rimosso", + "ADDED": "aggiunto", + "US_POINTS": "punti storia utente ({{name}})", + "NEW_ATTACHMENT": "nuovo allegato", + "DELETED_ATTACHMENT": "allegato eliminato", + "UPDATED_ATTACHMENT": "Aggiornato l'allegato {{filename}}", + "DELETED_CUSTOM_ATTRIBUTE": "elimina un attributo personalizzato", + "SIZE_CHANGE": "Fatto {size, plural, one{un cambiamento} other{# cambiamenti}}", + "VALUES": { + "YES": "si", + "NO": "no", + "EMPTY": "vuoto", + "UNASSIGNED": "non assegnato" + }, + "FIELDS": { + "SUBJECT": "oggetto", + "NAME": "nome", + "DESCRIPTION": "descrizione", + "CONTENT": "contenuto", + "STATUS": "stato", + "IS_CLOSED": "è chiuso?", + "FINISH_DATE": "Data di fine", + "TYPE": "tipo", + "PRIORITY": "priorità", + "SEVERITY": "criticità", + "ASSIGNED_TO": "assegnato a", + "WATCHERS": "Osservatori", + "MILESTONE": "sprint", + "USER_STORY": "storia utente", + "PROJECT": "progetto", + "IS_BLOCKED": "è bloccato", + "BLOCKED_NOTE": "nota bloccata", + "POINTS": "punti", + "CLIENT_REQUIREMENT": "Requisito del client", + "TEAM_REQUIREMENT": "Requisiti del team", + "IS_IOCAINE": "è un'aspirina", + "TAGS": "tag", + "ATTACHMENTS": "allegati", + "IS_DEPRECATED": "è deprecato", + "ORDER": "ordine", + "BACKLOG_ORDER": "Ordine di backlog", + "SPRINT_ORDER": "Ordine dello sprint", + "KANBAN_ORDER": "ordina kanban", + "TASKBOARD_ORDER": "Ordine del pannello dei compiti", + "US_ORDER": "Ordine delle storie utente" + } + }, + "BACKLOG": { + "PAGE_TITLE": "Backlog - {{projectName}}", + "PAGE_DESCRIPTION": "Il pannello di backlog, con le storie degli utenti e gli sprint di progetto", + "SECTION_NAME": "Backlog", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Amministratore", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", + "MOVE_US_TO_CURRENT_SPRINT": "Spostati allo sprint attuale", + "SHOW_FILTERS": "Mostra filtri", + "SHOW_TAGS": "Mostra tag", + "EMPTY": "Il tuo backlog è vuoto!", + "CREATE_NEW_US": "Crea una nuova Storia Utente", + "CREATE_NEW_US_EMPTY_HELP": "Potresti voler creare una nuova storia utente", + "EXCESS_OF_POINTS": "Eccesso di punti", + "PENDING_POINTS": "Punti in sospeso", + "CLOSED_POINTS": "chiuso", + "COMPACT_SPRINT": "Ripiega lo sprint", + "GO_TO_TASKBOARD": "Vai al pannello dei compiti di {{::name}}", + "EDIT_SPRINT": "Modifica lo sprint", + "TOTAL_POINTS": "totale", + "STATUS_NAME": "Nome dello status", + "SORTABLE_FILTER_ERROR": "Non puoi depositare in backlog quando i filtri sono aperti", + "DOOMLINE": "Scopo del progetto", + "CHART": { + "XAXIS_LABEL": "Sprints", + "YAXIS_LABEL": "Punti", + "OPTIMAL": "I punti in sospeso ottimali per lo sprint \"{{sprintName}}\" dovrebbero essere {{value}}", + "REAL": "I punti in sospeso definitivi per lo sprint \"{{sprintName}}\" sono {{value}}", + "INCREMENT_TEAM": "I punti incrementati dai requisiti del team per lo sprint \"{{sprintName}}\" sono {{value}}", + "INCREMENT_CLIENT": "I punti incrementati dai requisiti del cliente per lo sprint \"{{sprintName}}\" sono {{value}}" + }, + "TAGS": { + "TOGGLE": "Commuta visibilità tag", + "SHOW": "Mostra tag", + "HIDE": "Nascondi tag" + }, + "TABLE": { + "COLUMN_US": "Storie Utente", + "TITLE_COLUMN_POINTS": "Seleziona vista per Ruolo" + }, + "SPRINT_SUMMARY": { + "TOTAL_POINTS": "punti
totali", + "COMPLETED_POINTS": "
punti completati", + "OPEN_TASKS": "
compiti aperti", + "CLOSED_TASKS": "
compiti chiusi", + "IOCAINE_DOSES": "
pasticche di aspirina", + "SHOW_STATISTICS_TITLE": "Mostra statistiche", + "TOGGLE_BAKLOG_GRAPH": "Mostra/nascondi i grafici burndown" + }, + "SUMMARY": { + "PROJECT_POINTS": "
punti di progetto", + "DEFINED_POINTS": "
punti definiti", + "CLOSED_POINTS": "
punti chiusi", + "POINTS_PER_SPRINT": "
punti di sprint" + }, + "FILTERS": { + "TOGGLE": "Premi i filtri visibilità", + "TITLE": "Filtri", + "REMOVE": "Rimuovi filtri", + "HIDE": "Nascondi Filtri", + "SHOW": "Mostra Filtri", + "FILTER_CATEGORY_STATUS": "Stato", + "FILTER_CATEGORY_TAGS": "Tag" + }, + "SPRINTS": { + "TITLE": "SPRINTS", + "DATE": "DD MMM YYYY", + "LINK_TASKBOARD": "Pannello dei compiti dello sprint", + "TITLE_LINK_TASKBOARD": "Vai al pannello dei compiti di \"{{name}}\"", + "NUMBER_SPRINTS": "
sprints", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", + "ACTION_SHOW_CLOSED_SPRINTS": "Mostra gli sprints terminati", + "ACTION_HIDE_CLOSED_SPRINTS": "Nascondi lo sprint terminato" + } + }, + "ERROR": { + "TEXT1": "E' successo qualcosa, ma i nostri Digital Champions ci stanno lavorando sodo!", + "NOT_FOUND": "Non trovato", + "NOT_FOUND_TEXT": "Errore 404. La pagina che stai cercando non esiste più. Forse puoi tornare alla home e vedere se è possibile trovare quello che stai cercando.", + "PERMISSION_DENIED": "Permesso negato", + "PERMISSION_DENIED_TEXT": "Non si hanno i permessi necessari per accedere a questa pagina.", + "VERSION_ERROR": "Qualcuno all'interno di Taiga ha fatto un cambiamento prima che i nostri Digital Champion potessero applicare le modifiche. Per favore ricarica e applica di nuovo le modifiche (altrimenti andranno perse)." + }, + "TASKBOARD": { + "PAGE_TITLE": "{{sprintName}} - Pannello dei compiti dello Sprint - {{projectName}}", + "PAGE_DESCRIPTION": "Sprint {{sprintName}} (da {{startDate}} a {{endDate}}) di {{projectName}}. Completato {{completedPercentage}}% ({{completedPoints}} di {{totalPoints}} points). {{openTasks}} compiti aperti di {{totalTasks}}", + "SECTION_NAME": "Pannello dei compiti", + "TITLE_ACTION_ADD": "Aggiungi un nuovo compito", + "TITLE_ACTION_ADD_BULK": "Aggiungi qualche nuovo Compito nel carico", + "TITLE_ACTION_ASSIGN": "Compito assegnato", + "TITLE_ACTION_EDIT": "Modifica compito", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", + "TABLE": { + "COLUMN": "Storia utente", + "TITLE_ACTION_FOLD": "Ripiega la colonna", + "TITLE_ACTION_UNFOLD": "Riapri la colonna", + "TITLE_ACTION_FOLD_ROW": "Ripiega la riga", + "TITLE_ACTION_UNFOLD_ROW": "Rivela la riga", + "FIELD_POINTS": "punti", + "ROW_UNASSIGED_TASKS_TITLE": "Compito non assegnato" + }, + "CHARTS": { + "XAXIS_LABEL": "Giorni", + "YAXIS_LABEL": "Punti", + "OPTIMAL": "Il livello ottimale di punti sospesi per la giornata {{formattedDate}} dovrebbe essere {{roundedValue}}", + "REAL": "I punti in sospeso definitivi per la giornata {{formattedDate}} sono {{roundedValue}}", + "DATE": "DD MMMM YYYY" + } + }, + "TASK": { + "PAGE_TITLE": "{{taskSubject}} - Compiti {{taskRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Stato: {{taskStatus }}. Descrizione: {{taskDescription}}", + "SECTION_NAME": "Dettagli del compito", + "LINK_TASKBOARD": "Pannello dei compiti", + "TITLE_LINK_TASKBOARD": "Vai al pannello dei compiti", + "PLACEHOLDER_SUBJECT": "Inserisci il soggetto del nuovo compito", + "TITLE_SELECT_STATUS": "Nome dello status", + "OWNER_US": "Questo compito appartiene a", + "TITLE_LINK_GO_OWNER": "Vai alla storia utente", + "ORIGIN_US": "Questo compito è stato creato da", + "TITLE_LINK_GO_ORIGIN": "Vai alla storia utente", + "BLOCKED": "Questo compito è bloccato", + "PREVIOUS": "Compito precedente", + "NEXT": "Prossimo compito", + "TITLE_DELETE_ACTION": "Rimuovi compito", + "LIGHTBOX_TITLE_BLOKING_TASK": "Sto bloccando il compito", + "FIELDS": { + "MILESTONE": "Sprint", + "USER_STORY": "Storia utente", + "IS_IOCAINE": "E' un'aspirina" + }, + "ACTION_IOCAINE": "aspirina", + "TITLE_ACTION_IOCAINE": "Sei stremato? Assicurati che gli altri lo sappiano cliccando su 'aspirina' quando stai lavorando su compito che ti affatica. Ma non demordere, ricordati: si può migliorare solo se di tanto in tanto si accettano sfide extra!" + }, + "NOTIFICATION": { + "OK": "E' tutto ok!", + "WARNING": "Oops, é successo qualcosa...", + "WARNING_TEXT": "Accidenti! I nostri Digital Champions sono tristi..le modifiche non sono state salvate", + "SAVED": "I nostri Digital Champions hanno salvato tutte le modifiche! wow!!", + "CLOSE": "Chiudi notifica", + "MAIL": "Notifica tramite mail", + "ASK_DELETE": "Sei sicuro di voler cancellare?" + }, + "CANCEL_ACCOUNT": { + "TITLE": "Elimina il tuo account", + "SUBTITLE": "Ci spiace se stai lasciando Taiga, speriamo che il tuo soggiorno sia stato piacevole :)", + "PLACEHOLDER_INPUT_TOKEN": "cancella il token dell'account", + "ACTION_LEAVING": "Si, sto abbandonando!", + "SUCCESS": "I nostri Digital Champions hanno eliminato il tuo account, e ora sono tristi" + }, + "CHANGE_EMAIL_FORM": { + "TITLE": "Cambia la tua email", + "SUBTITLE": "Un'altro click e la tua mail verrá aggiornata!", + "PLACEHOLDER_INPUT_TOKEN": "Modifica il token della mail", + "ACTION_CHANGE_EMAIL": "Cambia email", + "SUCCESS": "I nostri Digital Champions hanno aggiornato la tua mail" + }, + "ISSUES": { + "PAGE_TITLE": "Criticitá - {{projectName}}", + "PAGE_DESCRIPTION": "Il pannello con la lista dei problemi del progetto {{projectName}}: {{projectDescription}}", + "LIST_SECTION_NAME": "problemi", + "SECTION_NAME": "Dettagli della criticitá", + "ACTION_NEW_ISSUE": "+ NUOVA CRITICITÁ", + "ACTION_PROMOTE_TO_US": "Promuovi la storia utente", + "PLACEHOLDER_FILTER_NAME": "Scrivi il nome del filtro e premi invio", + "PROMOTED": "Il problema è stato promosso a storia utente", + "EXTERNAL_REFERENCE": "Questo problema è stato creato da ", + "GO_TO_EXTERNAL_REFERENCE": "Ritorna all'inizio", + "BLOCKED": "Questo problema è bloccato", + "TITLE_PREVIOUS_ISSUE": "problema precedente", + "TITLE_NEXT_ISSUE": "Problema successivo", + "ACTION_DELETE": "Elimina problema", + "LIGHTBOX_TITLE_BLOKING_ISSUE": "Issue bloccante", + "FIELDS": { + "PRIORITY": "Priorità", + "SEVERITY": "Gravità", + "TYPE": "Tipo" + }, + "CONFIRM_PROMOTE": { + "TITLE": "Promuovi questo problema come nuova storia utente", + "MESSAGE": "Sei sicuro di voler creare una nuova storia utente da questo problema?" + }, + "FILTERS": { + "TITLE": "Filtri", + "INPUT_SEARCH_PLACEHOLDER": "Soggetto o referenza", + "TITLE_ACTION_SEARCH": "Cerca", + "ACTION_SAVE_CUSTOM_FILTER": "salva come filtro personalizzato", + "BREADCRUMB": "Filtri", + "TITLE_BREADCRUMB": "Filtri", + "CATEGORIES": { + "TYPE": "Tipo", + "STATUS": "Stato", + "SEVERITY": "Gravità", + "PRIORITIES": "priorità", + "TAGS": "Tag", + "ASSIGNED_TO": "Assegna a", + "CREATED_BY": "Creato da", + "CUSTOM_FILTERS": "Filtri personalizzati" + }, + "CONFIRM_DELETE": { + "TITLE": "Elimina il filtro personalizzato", + "MESSAGE": "Il filtro personalizzato '{{customFilterName}}'" + } + }, + "TABLE": { + "COLUMNS": { + "TYPE": "Tipo", + "SEVERITY": "Gravità", + "PRIORITY": "Priorità", + "SUBJECT": "Oggetto", + "VOTES": "Voti", + "STATUS": "Stato", + "CREATED": "Creato", + "ASSIGNED_TO": "Assegna a" + }, + "TITLE_ACTION_CHANGE_STATUS": "Cambia stato", + "TITLE_ACTION_ASSIGNED_TO": "Assegna a", + "BLOCKED": "Bloccato", + "EMPTY": { + "TITLE": "Non ci sono problemi da segnalare :-)", + "SUBTITLE": "Hai trovato un problema?" + } + } + }, + "ISSUE": { + "PAGE_TITLE": "{{issueSubject}} - Criticitá {{issueRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Stato: {{issueStatus }}. Tipo: {{issueType}}, Prioritá: {{issuePriority}}. Severitá: {{issueSeverity}}. Descrizione: {{issueDescription}}" + }, + "KANBAN": { + "PAGE_TITLE": "Kanban - {{projectName}}", + "PAGE_DESCRIPTION": "Il pannello kanban, con le storie utenti del progetto {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Kanban", + "TITLE_ACTION_FOLD": "Ripiega la colonna", + "TITLE_ACTION_UNFOLD": "Riapri la colonna", + "TITLE_ACTION_FOLD_CARDS": "ripiega la scheda", + "TITLE_ACTION_UNFOLD_CARDS": "Apri le carte", + "TITLE_ACTION_ADD_US": "Aggiungi una nuova storia utente", + "TITLE_ACTION_ADD_BULK": "Aggiungi un nuovo carico", + "ACTION_SHOW_ARCHIVED": "Mostra archivio", + "ACTION_HIDE_ARCHIVED": "Nascondi archivio", + "HIDDEN_USER_STORIES": "Le storie utente in questo status sono nascoste by default", + "ARCHIVED": "Hai archiviato", + "UNDO_ARCHIVED": "Trascina e lascia di nuovo per annullare", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" + }, + "SEARCH": { + "PAGE_TITLE": "Cerca - {{projectName}}", + "PAGE_DESCRIPTION": "Cerca storie utenti, problemi, compiti o pagine wiki, all'interno del progetto {{projectName}}: {{projectDescription}}", + "FILTER_USER_STORIES": "Storie Utente", + "FILTER_ISSUES": "problemi", + "FILTER_TASKS": "Compiti", + "FILTER_WIKI": "Pagine wiki", + "PLACEHOLDER_SEARCH": "Cerca in...", + "TITLE_ACTION_SEARCH": "cerca", + "EMPTY_TITLE": "Sembra che non ci sia nulla che corrisponda ai criteri ricerca", + "EMPTY_DESCRIPTION": "Prova uno dei tab che trovi sopra, o prova a cercare di nuovo" + }, + "TEAM": { + "PAGE_TITLE": "Team - {{projectName}}", + "PAGE_DESCRIPTION": "Il pannello del team da mostrare a tutti i membri del progetto {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Squadra", + "APP_TITLE": "TEAM - {{projectName}}", + "PLACEHOLDER_INPUT_SEARCH": "Cerca con il nome completo", + "COLUMN_MR_WOLF": "Mr. Wolf", + "EXPLANATION_COLUMN_MR_WOLF": "Problema chiuso", + "COLUMN_IOCAINE": "aspirinomane", + "EXPLANATION_COLUMN_IOCAINE": "presa una pasticca di aspirina", + "COLUMN_CERVANTES": "Cervantes", + "EXPLANATION_COLUMN_CERVANTES": "Modificata pagina wiki", + "COLUMN_BUG_HUNTER": "Cacciatore di bug", + "EXPLANATION_COLUMN_BUG_HUNTER": "Problemi segnalati", + "COLUMN_NIGHT_SHIFT": "Turno notturno", + "EXPLANATION_COLUMN_NIGHT_SHIFT": "Compiti chiusi", + "COLUMN_TOTAL_POWER": "Potere totale", + "EXPLANATION_COLUMN_TOTAL_POWER": "Totale Punti", + "SECTION_TITLE_TEAM": "Squadra >", + "SECTION_FILTER_ALL": "Tutti", + "CONFIRM_LEAVE_PROJECT": "Sei sicuro di voler abbandonare il progetto?", + "ACTION_LEAVE_PROJECT": "Abbandona il progetto" + }, + "USER_SETTINGS": { + "AVATAR_MAX_SIZE": "[Dimensione massima: {{maxFileSize}}]", + "MENU": { + "SECTION_TITLE": "Impostazioni utente", + "USER_PROFILE": "Profilo utente", + "CHANGE_PASSWORD": "Cambia password", + "EMAIL_NOTIFICATIONS": "Notifiche email" + }, + "NOTIFICATIONS": { + "SECTION_NAME": "Notifiche email", + "COLUMN_PROJECT": "Progetto", + "COLUMN_RECEIVE_ALL": "Ricevi tutto", + "COLUMN_ONLY_INVOLVED": "Solo coinvolto", + "COLUMN_NO_NOTIFICATIONS": "Nessuna notifica", + "OPTION_ALL": "Tutti", + "OPTION_INVOLVED": "Coinvolto", + "OPTION_NONE": "Nessuno" + }, + "POPOVER": { + "USER_PROFILE": "Profilo utente", + "CHANGE_PASSWORD": "Cambia Password", + "NOTIFICATIONS": "Notifiche", + "FEEDBACK": "Feedback", + "TITLE_AVATAR": "Preferenze utente" + } + }, + "USER_PROFILE": { + "IMAGE_HELP": "L'immagine verrà scalata a 80x80px.
", + "ACTION_CHANGE_IMAGE": "Cambia", + "ACTION_USE_GRAVATAR": "Usa gravatar", + "ACTION_DELETE_ACCOUNT": "Elimina l'account Taiga", + "CHANGE_EMAIL_SUCCESS": "Controlla la tua casella di posta
abbiamo mandato una mail al tuo account
con le istruzioni per impostare un nuovo indirizzo", + "CHANGE_PHOTO": "Cambia foto", + "FIELD": { + "USERNAME": "Username", + "EMAIL": "Email", + "FULL_NAME": "Nome completo", + "PLACEHOLDER_FULL_NAME": "Inserisci il tuo nome completo (es. Alessio Biancalana)", + "BIO": "Biografia (massimo 210 caratteri)", + "PLACEHOLDER_BIO": "Raccontaci qualcosa di te", + "LANGUAGE": "Lingua", + "LANGUAGE_DEFAULT": "-- usa lingua predefinita --", + "THEME": "Tema", + "THEME_DEFAULT": "-- usa tema predefinito --" + } + }, + "WIZARD": { + "SECTION_TITLE_CHOOSE_TEMPLATE": "Scegli il tema", + "CHOOSE_TEMPLATE_TEXT": "Quale template si adatta meglio al tuo progetto?", + "SECTION_TITLE_CREATE_PROJECT": "Crea Progetto", + "CREATE_PROJECT_TEXT": "Nuovo di zecca. Vai così!", + "PROGRESS_TEMPLATE_SELECTION": "Selezione del template", + "PROGRESS_NAME_DESCRIPTION": "Nome e descrizione" + }, + "WIKI": { + "PAGE_TITLE": "{{wikiPageName}} - Wiki - {{projectName}}", + "PAGE_DESCRIPTION": "L'ultima versione su {{lastModifiedDate}} ({{totalEditions}} versioni in totale) Contenuto: {{ wikiPageContent }}", + "DATETIME": "DD MMM YYYY HH:mm", + "PLACEHOLDER_PAGE": "Crea la tua pagina wiki", + "REMOVE": "Rimuovi questa pagina wiki", + "DELETE_LIGHTBOX_TITLE": "Elimina Pagina Wiki", + "NAVIGATION": { + "SECTION_NAME": "Link", + "ACTION_ADD_LINK": "Aggiungi link" + }, + "SUMMARY": { + "TIMES_EDITED": "tempo
modificato", + "LAST_EDIT": "
ultima modifica", + "LAST_MODIFICATION": "ultima modifica" + } + }, + "HINTS": { + "SECTION_NAME": "Suggerimento", + "LINK": "Se vuoi sapere come si usa visita la nostra pagina di supporto", + "LINK_TITLE": "Visita la nostra pagina di supporto", + "HINT1_TITLE": "Sai che puoi anche importare ed esportare progetti?", + "HINT1_TEXT": "Questo ti permette di estrarre tutti i tuoi dati da un Taiga e muoverli dentro un altro.", + "HINT2_TITLE": "Sai che puoi anche creare dei cambi personalizzati?", + "HINT2_TEXT": "Ora il team puà creare un nuovo campo personalizzato come strumento flessibile per inserire dati specificamente utili per alcuni flussi di lavoro", + "HINT3_TITLE": "Riordina i tuoi progetti per includere quelli più rilevanti", + "HINT3_TEXT": "La top 10 dei progetti sarà visibile in alto nella barra ad accesso diretto", + "HINT4_TITLE": "Ti sei dimenticato su cosa stavi lavorando?", + "HINT4_TEXT": "Non preoccuparti, sulla tua dashboard troverai i compiti aperti, i problemi e le storie utenti così da poterci lavorare" + }, + "TIMELINE": { + "UPLOAD_ATTACHMENT": "{{username}} ha caricato un nuovo allegato in {{obj_name}}", + "US_CREATED": "{{username}} ha creato una nuova storia utente {{obj_name}} in {{project_name}}", + "ISSUE_CREATED": "{{username}} ha creato un nuovo problema {{obj_name}} in {{project_name}}", + "TASK_CREATED": "{{username}} ha creato un nuovo compito {{obj_name}} in {{project_name}}", + "TASK_CREATED_WITH_US": "{{username}} ha creato un nuovo compito {{obj_name}} in {{project_name}} che appartiene alla storia utente {{us_name}}", + "WIKI_CREATED": "{{username}} ha creato una nuova pagina wiki {{obj_name}} in {{project_name}}", + "MILESTONE_CREATED": "{{username}} ha creato un nuovo sprint {{obj_name}} in {{project_name}}", + "NEW_PROJECT": "{{username}} ha creato il progetto {{project_name}}", + "MILESTONE_UPDATED": "{{username}} ha aggiornato lo sprint {{obj_name}}", + "US_UPDATED": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" alla storia utente {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" della storia utente {{obj_name}} a {{new_value}}", + "US_UPDATED_POINTS": "{{username}} ha aggiornato '{{role_name}}' punti della storia utente {{obj_name}} a {{new_value}}", + "ISSUE_UPDATED": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del problema {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del problema {{obj_name}} a {{new_value}}", + "TASK_UPDATED": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del compito {{obj_name}} a {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del compito {{obj_name}} a {{new_value}}", + "TASK_UPDATED_WITH_US": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del compito {{obj_name}} che appartiene alla storia utente {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} ha aggiornato l'attributo \"{{field_name}}\" del compito {{obj_name}} che appartiene alla storia utente {{us_name}} a {{new_value}}", + "WIKI_UPDATED": "{{username}} ha aggiornato la pagina wiki {{obj_name}}", + "NEW_COMMENT_US": "{{username}} ha commentato nella storia utente {{obj_name}}", + "NEW_COMMENT_ISSUE": "{{username}} ha commentato nel problema {{obj_name}}", + "NEW_COMMENT_TASK": "{{username}} ha commentato nel compito {{obj_name}}", + "NEW_MEMBER": "{{project_name}} ha un nuovo membro", + "US_ADDED_MILESTONE": "{{username}} ha aggiunto la storia utente {{obj_name}} a {{sprint_name}}", + "US_MOVED": "{{username}} ha spostato la storia utente {{obj_name}}", + "US_REMOVED_FROM_MILESTONE": "{{username}} ha aggiunto la storia utente {{obj_name}} al backlog", + "BLOCKED": "{{username}} è stato bloccato {{obj_name}}", + "UNBLOCKED": "{{username}} è stato sbloccato {{obj_name}}", + "NEW_USER": "{{username}} si é iscritto su Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "Un'applicazione esterna richiede l'autenticazione", + "PAGE_DESCRIPTION": "Un'applicazione esterna richiede l'autenticazione", + "AUTHORIZATION_REQUEST": "Vuoi autorizzare {{application}} ad usare il tuo account Taiga?", + "LOGIN_WITH_ANOTHER_USER": "Accedi come un altro utente", + "AUTHORIZE_APP": "Autorizza applicazione", + "CANCEL": "Annulla" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Your project", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Sta lavorando su", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "In osservazione", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "Storie Utente", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } + } +} \ No newline at end of file diff --git a/app/locales/locale-nl.json b/app/locales/locale-nl.json index 02baf2ab..44df576b 100644 --- a/app/locales/locale-nl.json +++ b/app/locales/locale-nl.json @@ -36,6 +36,7 @@ "EXTERNAL_USER": "een extern gebruiker", "GENERIC_ERROR": "Een van onze Oempa Loempa's zegt {{error}}.", "IOCAINE_TEXT": "Voel je je wat overweldigd door een taak? Zorg ervoor dat anderen dit weten door op Iocaine te klikken bij het wijzigen van een taak. Je kan stapsgewijs immuun worden tegen dit (fictioneel) dodelijk gif door kleine dosissen op te nemen. Net zoals je beter kan worden in wat je doet door af en toe een extra uitdaging aan te gaan!", + "CAPSLOCK_WARNING": "Let op! Je schrijft met alleen hoofdletters en dit veld is hoofdlettergevoelig.", "FORM_ERRORS": { "DEFAULT_MESSAGE": "Deze waarde lijkt ongeldig te zijn", "TYPE_EMAIL": "Deze waarde moet een geldig emailadres bevatten", @@ -130,7 +131,8 @@ "POINTS": "Punten", "BLOCKED_NOTE": "geblokkeerde notitie", "IS_BLOCKED": "is geblokkeerd", - "REF": "Ref" + "REF": "Ref", + "VOTES": "Stemmen" }, "ROLES": { "ALL": "Alles" @@ -148,11 +150,27 @@ "OPEN": "Open" }, "WATCHERS": { - "ADD": "Waarnemer toevoegen", - "TITLE": "waarnemers", + "ADD": "Add watchers", + "TITLE_ADD": "Add a project member to the watchers list", "DELETE": "Verwijder waarnemer", "TITLE_LIGHTBOX_DELETE_WARTCHER": "Verwijder waarnemer..." }, + "WATCH_BUTTON": { + "WATCH": "Watch", + "WATCHING": "Volgers", + "UNWATCH": "Unwatch", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Watch/Unwatch this item", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Upvoted", + "DOWNVOTE": "Downvote", + "VOTERS": "Voters", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "Eigen velden", "SAVE": "Eigen velden opslaan", @@ -307,12 +325,10 @@ "CHANGE_PASSWORD_RECOVERY_FORM": { "TITLE": "Maak een nieuwe Taiga pass aan", "SUBTITLE": "En hé, misschien dat je wat ijzer-rijk voedsel kunt eten, het is goed voor je brein :P", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Wachtwoord token herstellen", - "LINK_NEED_TOKEN": "Één nodig?", - "TITLE_LINK_NEED_TOKEN": "Had je een token nodig om je wachtwoord te recupereren, omdat je het vergeten was?", "PLACEHOLDER_NEW_PASSWORD": "Nieuw wachtwoord", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Herhaal wachtwoord", "ACTION_RESET_PASSWORD": "Wachtwoord resetten", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", "SUCCESS": "Onze Oempa-Loempas hebben je nieuwe wachtwoord opgeslagen.
Probeer ermee in te loggen." }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "Accepteer de uitnodiging voor een project in Taiga, een project management platform voor startups en agile ontwikkelaars & designers die een simpele, mooie tool willen om werken echt leuk te maken." }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Ooeps, we hebben een probleem
Onze Oempa-Loempa's kunnen je uitnodiging niet vinden.", + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", "SUCCESS": "Je bent met succes toegetreden tot dit project, Welkom bij {{project_name}}", "ERROR": "Volgens onze Oempa-Loempa's, ben je nog niet geregistreerd of heb je een fout wachtwoord getypt." }, "HOME": { "PAGE_TITLE": "Home -Taiga", "PAGE_DESCRIPTION": "De Taiga start pagina met jouw projecten en de aan jou toegewezen en gevolgde user stories, taken en issues", - "EMPTY_WATCHING": "Volg de projecten, User Stories, Taken, Issues... waarvan je op de hoogte gehouden wilt worden :)", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", "EMPTY_PROJECT_LIST": "Je hebt nog geen projecten", "WORKING_ON_SECTION": "Werkt aan", "WATCHING_SECTION": "Volgers" @@ -343,7 +360,7 @@ "DESCRIPTION": "Geef een korte beschrijving", "DEPRECATED": "(verouderd)", "DEPRECATED_FILE": "Verouderd?", - "ADD": "Nieuwe bijlage toevoegen. <%- maxFileSizeMsg %>", + "ADD": "Nieuwe bijlage toevoegen. {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Max. grootte: {{maxFileSize}}]", "SHOW_DEPRECATED": "+ toon verouderde bijlagen", "HIDE_DEPRECATED": "- verberg verouderde bijlagen", @@ -407,15 +424,20 @@ "MEETUP": "Meet Up", "MEETUP_DESCRIPTION": "Kies je videoconference systeem. Zelfs programmeurs hebben face-to-face contact nodig.", "SELECT_VIDEOCONFERENCE": "Kies een videoconference systeem", - "SALT_CHAT_ROOM": "Als je wil kan je een salt code aan de naam van de chatroom toevoegen" + "SALT_CHAT_ROOM": "Als je wil kan je een salt code aan de naam van de chatroom toevoegen", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Op maat", + "URL_CHAT_ROOM": "URL van je chat room" }, "PROJECT_PROFILE": { "PAGE_TITLE": "{{sectionName}} - Project profiel - {{projectName}}", "PROJECT_DETAILS": "Project details", "PROJECT_NAME": "Naam project", "PROJECT_SLUG": "Project Slug", - "NUMBER_SPRINTS": "Aantal sprints", - "NUMBER_US_POINTS": "Aantal US punten", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", "TAGS": "Tags", "DESCRIPTION": "Beschrijving", "PUBLIC_PROJECT": "Open project", @@ -425,7 +447,7 @@ "REPORTS": { "TITLE": "Rapporten", "SUBTITLE": "Exporteer je project data in CSV formaat en maak je eigen rapporten", - "DESCRIPTION": "Download een CSV bestand of kopieer de gegenereerde URL en open het in je favoriete tekstverwerker of spreadsheet om je eigen project data rapporten aan te maken. Je zal al je data makkelijk kunnen visualiseren en analyseren.", + "DESCRIPTION": "Download een CSV bestand of kopieer de gegenereerde URL en open deze in je favoriete tekst editor of spreadsheet om je eigen rapportages te maken. Op die manier kun je al je data gemakkelijk analyseren en visualiseren.", "HELP": "Hoe kan ik dit gebruiken in mijn eigen spreadsheet?", "REGENERATE_TITLE": "Wijzig URL", "REGENERATE_SUBTITLE": "Je staat op het punt de CSV data toegang url te veranderen. De vorige url zal worden uitgeschakeld. Ben je zeker dat je ermee door wil gaan?" @@ -448,7 +470,10 @@ "TASK_DESCRIPTION": "Eigen velden taken", "TASK_ADD": "Voeg een aangepast veld toe in taken", "ISSUE_DESCRIPTION": "Issues aangepaste velden", - "ISSUE_ADD": "Voeg een aangepast veld toe in issues" + "ISSUE_ADD": "Voeg een aangepast veld toe in issues", + "FIELD_TYPE_TEXT": "Tekst", + "FIELD_TYPE_MULTI": "Over meerdere regels", + "FIELD_TYPE_DATE": "Date" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - Project waardes - {{projectName}}", @@ -490,6 +515,9 @@ "PAGE_TITLE": "Rollen - {{projectName}}", "WARNING_NO_ROLE": "Wees voorzichtig, geen enkele rol in je project zal de puntenwaarde van een user story kunnen estimeren", "HELP_ROLE_ENABLED": "Als dit actief is, zullen leden met deze rol de punten waarde voor user stories kunnen schatten", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", "COUNT_MEMBERS": "{{ role.members_count }} leden met deze rol", "TITLE_DELETE_ROLE": "Verwijder Rol", "REPLACEMENT_ROLE": "Al de gebruikers met deze rol zullen verplaats worden naar", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "Actief", "STATUS_PENDING": "Resterend", "DELETE_MEMBER": "Verwijder gebruiker", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "We hebben de uitnodiging opnieuw gestuurd aan '{{email}}'.", "ERROR_SEND_INVITATION": "We hebben je een uitnodiging gestuurd", "SUCCESS_DELETE": "We hebben {{message}} verwijderd.", @@ -626,23 +655,47 @@ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", "EDIT": "Profiel bewerken", "FOLLOW": "Volgen", - "PROJECTS": "Projecten", "CLOSED_US": "Gesloten US", + "PROJECTS": "Projecten", + "PROJECTS_EMPTY": "{{username}} heeft nog geen projecten", "CONTACTS": "Contacten", - "REPORT": "Meld misbruik", - "ACTIVITY_TAB": "Activiteiten tab", - "PROJECTS_TAB": "Projecten tab", - "CONTACTS_TAB": "Contacten tab", - "FAVORITES_TAB": "Favorieten tab", "CONTACTS_EMPTY": "{{username}} heeft nog geen contacten", "CURRENT_USER_CONTACTS_EMPTY": "Je hebt nog geen contacten", "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "De mensen waarmee je via Taiga werkt zullen automatisch je contacten worden", - "PROJECTS_EMPTY": "{{username}} heeft nog geen projecten" + "REPORT": "Meld misbruik", + "TABS": { + "ACTIVITY_TAB": "Activiteit", + "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "PROJECTS_TAB": "Projecten", + "PROJECTS_TAB_TITLE": "List all projects that this user is a member", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Stemmen", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Watched", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Contacten", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } }, "PROFILE_SIDEBAR": { "TITLE": "Jouw profiel", "DESCRIPTION": "Mensen kunnen alles zien wat je doet en waar je aan werkt. Voeg een mooie biografie toe voor een meer uitgebreide versie van jouw informatie.", - "ADD_INFO": "Bewerk je bio" + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Type something...", + "FILTER_TYPE_ALL": "Alles", + "FILTER_TYPE_ALL_TITLE": "Show all", + "FILTER_TYPE_PROJECTS": "Projecten", + "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", + "FILTER_TYPE_TASKS": "Taken", + "FILTER_TYPE_TASK_TITLES": "Show only tasks", + "FILTER_TYPE_ISSUES": "Issues", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." } }, "PROJECT": { @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "Onze Oempa-Loempa's hebben wat problemen met het importeren van je dump: {{error_message}}", "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) is te zwaar voor onze Oempa-Loempa's, probeer het met een bestand kleiner dan ({{maxFileSize}})", "SYNC_SUCCESS": "Je project werd met succes geïmporteerd" + }, + "LIKE_BUTTON": { + "LIKE": "Like", + "LIKED": "Liked", + "UNLIKE": "Unlike", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Watch", + "WATCHING": "Volgers", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Receive all notifications", + "NOTIFY_ALL_TITLE": "Receive all notifications for this project", + "NOTIFY_INVOLVED": "Only involved", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Unwatch", + "UNWATCH_TITLE": "Unwatch this project" + } } }, "LIGHTBOX": { @@ -783,7 +857,7 @@ "SECTION_NAME": "User story details", "LINK_TASKBOARD": "Taakbord", "TITLE_LINK_TASKBOARD": "Ga naar het dashboard", - "TOTAL_POINTS": "totaal", + "TOTAL_POINTS": "total points", "ADD": "+ Nieuwe User Story toevoegen", "ADD_BULK": "Voeg enkele nieuwe User Stories in bulk toe", "PROMOTED": "Deze US werd gepromoveerd van Issue:", @@ -794,7 +868,7 @@ "PREVIOUS": "Vorige user story", "NEXT": "volgende user story", "TITLE_DELETE_ACTION": "Verwijder user story", - "LIGHTBOX_TITLE_BLOKING_US": "Ons blokkeren", + "LIGHTBOX_TITLE_BLOKING_US": "User story blokkeren", "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} taken afgewerkt", "ASSIGN": "User story toewijzen", "NOT_ESTIMATED": "Niet ingeschat", @@ -870,6 +944,10 @@ "PAGE_TITLE": "Backlog - {{projectName}}", "PAGE_DESCRIPTION": "Het backlog overzicht, met user stories en sprints van het project {{projectName}}: {{projectDescription}}", "SECTION_NAME": "Backlog", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Admin", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "Verplaats naar huidige sprint", "SHOW_FILTERS": "Toon filters", "SHOW_TAGS": "Toon tags", @@ -889,10 +967,10 @@ "CHART": { "XAXIS_LABEL": "Sprints", "YAXIS_LABEL": "Punten", - "OPTIMAL": "Optimale aantal resterende punten voor sprint {{xval}} zou {{yval}} zijn", - "REAL": "Werkelijk aantal resterende punten voor sprint {{xval}} is {{yval}}", - "INCREMENT_TEAM": "Aantal toegevoegde punten door requirements van het team voor sprint {{xval}} is {{yval}}", - "INCREMENT_CLIENT": "Aantal toegevoegde punten door requirements van de klant voor sprint {{xval}} is {{yval}}" + "OPTIMAL": "Optimaal aantal openstaande punten voor sprint \"{{sprintName}}\" zou {{value}} moeten zijn", + "REAL": "Reële openstaande punten voor sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_TEAM": "Toegevoegde aantal punten door team vereisten voor sprint \"{{sprintName}}\" is {{vallue}}", + "INCREMENT_CLIENT": "Toegevoegde aantal punten door klant vereisten voor sprint \"{{sprintName}}\" is {{vallue}}" }, "TAGS": { "TOGGLE": "Zet de tag zichtbaarheid aan of af", @@ -909,7 +987,8 @@ "OPEN_TASKS": "open
taken", "CLOSED_TASKS": "gesloten
taken", "IOCAINE_DOSES": "iocaine
dosissen", - "SHOW_STATISTICS_TITLE": "Toon statistieken" + "SHOW_STATISTICS_TITLE": "Toon statistieken", + "TOGGLE_BAKLOG_GRAPH": "Toon/Verstop burndown grafiek" }, "SUMMARY": { "PROJECT_POINTS": "project
punten", @@ -932,19 +1011,20 @@ "LINK_TASKBOARD": "Sprint taakbord", "TITLE_LINK_TASKBOARD": "Ga naar taakbord van \"{{name}}\"", "NUMBER_SPRINTS": "
sprints", - "TITLE_ACTION_NEW_SPRINT": "+ Nieuwe sprint", - "ACTION_NEW_SPRINT": "+ Nieuwe sprint", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", "ACTION_SHOW_CLOSED_SPRINTS": "Toon gesloten sprints", "ACTION_HIDE_CLOSED_SPRINTS": "Verberg afgesloten sprints" } }, "ERROR": { "TEXT1": "Er is iets gebeurd en onze Oempa-Loempa's zijn er mee bezig.", - "TEXT2": "Probeer binnenkort nog eens te herladen.", "NOT_FOUND": "Niet gevonden", "NOT_FOUND_TEXT": "Error 404. De pagina die je zoekt bestaat niet meer. Misschien kan je terugkeren naar de TAIGA startpagina en kijken of je kan vinden wat je zoekt.", "PERMISSION_DENIED": "Niet toegestaan", - "PERMISSION_DENIED_CODE": "Error 403", + "PERMISSION_DENIED_TEXT": "You don't have permission to access to this page.", "VERSION_ERROR": "Iemand bij Taiga heeft dit vooraf veranderd en onze Oempa-Loempa's konden je veranderingen niet toepassen. Gelieve te herladen en je veranderingen opnieuw toe te passen (ze zullen verloren gaan)" }, "TASKBOARD": { @@ -955,6 +1035,8 @@ "TITLE_ACTION_ADD_BULK": "Wat nieuwe taken in bulk toevoegen", "TITLE_ACTION_ASSIGN": "Toegewezen taak", "TITLE_ACTION_EDIT": "Bewerk taak", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", "TABLE": { "COLUMN": "User story", "TITLE_ACTION_FOLD": "Kolom inklappen", @@ -1073,16 +1155,17 @@ "SEVERITY": "Ernst", "PRIORITY": "Prioriteit", "SUBJECT": "Onderwerp", + "VOTES": "Stemmen", "STATUS": "Status", "CREATED": "Aangemaakt", "ASSIGNED_TO": "Toegewezen aan" }, "TITLE_ACTION_CHANGE_STATUS": "Status veranderen", "TITLE_ACTION_ASSIGNED_TO": "Toegewezen aan", + "BLOCKED": "Geblokkeerd", "EMPTY": { "TITLE": "Er zijn geen issues om te rapporteren :-)", - "SUBTITLE": "Heb je een issue gevonden?", - "ACTION_CREATE_ISSUE": "Nieuw issue aanmaken" + "SUBTITLE": "Heb je een issue gevonden?" } } }, @@ -1104,7 +1187,9 @@ "ACTION_HIDE_ARCHIVED": "Verberg gearchiveerde", "HIDDEN_USER_STORIES": "De user stories met deze status zijn standaard verborgen", "ARCHIVED": "Je hebt gearchiveerd", - "UNDO_ARCHIVED": "Drag & drop nog een keer om ongedaan te maken" + "UNDO_ARCHIVED": "Drag & drop nog een keer om ongedaan te maken", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" }, "SEARCH": { "PAGE_TITLE": "Zoek - {{projectName}}", @@ -1182,7 +1267,9 @@ "BIO": "Bio", "PLACEHOLDER_BIO": "Vertel iets over jezelf", "LANGUAGE": "Taal", - "LANGUAGE_DEFAULT": "-- gebruik standaard taal --" + "LANGUAGE_DEFAULT": "-- gebruik standaard taal --", + "THEME": "Thema", + "THEME_DEFAULT": "-- gebruikt standaard thema --" } }, "WIZARD": { @@ -1234,18 +1321,91 @@ "NEW_PROJECT": "{{username}} heeft een nieuw project aangemaakt {{project_name}}", "MILESTONE_UPDATED": "{{username}} heeft de sprint {{obj_name}} bijgewerkt", "US_UPDATED": "{{username}} heeft de eigenschap \"{{field_name}}\" van de US {{obj_name}} bijgewerkt", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} heeft het attribuut \"{{field_name}}\" van de US {{obj_name}} gewijzigd naar {{new_value}}", + "US_UPDATED_POINTS": "{{username}} heeft '{{role_name}}' punten van de user story {{obj_name}} gewijzigd naar {{new_value}}", "ISSUE_UPDATED": "{{username}} heeft de eigenschap \"{{field_name}}\" van het issue {{obj_name}} bijgewerkt", - "TASK_UPDATED": "{{username}} heeft de eigenschap \"{{field_name}}\" van de taak {{obj_name}} bijgewerkt", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} heeft het attribuut \"{{field_name}}\" van de issue gewijzigd van {{obj_name}} naar {{new_value}}", + "TASK_UPDATED": "{{username}} heeft het attribuut \"{{field_name}}\" van de taak {{obj_name}} gewijzigd naar {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} heeft het attribuut \"{{field_name}}\" van de taak {{obj_name}} gewijzigd naar {{new_value}}", "TASK_UPDATED_WITH_US": "{{username}} heeft de eigenschap \"{{field_name}}\" van de taak {{obj_name}} die behoort tot de US {{us_name}} bijgewerkt", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} heeft de eigenschap \"{{field_name}}\" van de taak {{obj_name}} die behoort tot de US {{us_name}} gewijzigd naar {{new_value}}", "WIKI_UPDATED": "{{username}} heeft de wiki pagina {{obj_name}} bijgewerkt", "NEW_COMMENT_US": "{{username}} heeft gereageerd op de US {{obj_name}}", "NEW_COMMENT_ISSUE": "{{username}} heeft gereageerd op het issue {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} heeft gereageerd op de taak {{obj_name}}", "NEW_MEMBER": "{{project_name}} heeft een nieuw lid", "US_ADDED_MILESTONE": "{{username}} heeft de US {{obj_name}} toegevoegd aan {{sprint_name}}", + "US_MOVED": "{{username}} heeft de user story {{obj_name}} verplaatst", "US_REMOVED_FROM_MILESTONE": "{{username}} heeft de US {{obj_name}} toegevoegd aan de Backlog", "BLOCKED": "{{username}} heeft {{obj_name}} geblokkeerd", "UNBLOCKED": "{{username}} heeft {{obj_name}} gedeblokkeerd", "NEW_USER": "{{username}} heeft zich aangemeld voor Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "An external app requires authentication", + "PAGE_DESCRIPTION": "An external app requires authentication", + "AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?", + "LOGIN_WITH_ANOTHER_USER": "Login with another user", + "AUTHORIZE_APP": "Authorize app", + "CANCEL": "Annuleren" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Your project", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Werkt aan", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Volgers", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "User Stories", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } } } \ No newline at end of file diff --git a/app/locales/locale-pl.json b/app/locales/locale-pl.json new file mode 100644 index 00000000..d5789e80 --- /dev/null +++ b/app/locales/locale-pl.json @@ -0,0 +1,1411 @@ +{ + "COMMON": { + "YES": "Tak", + "NO": "Nie", + "LOADING": "Wczytywanie...", + "LOADING_PROJECT": "Wczytywanie projektu...", + "DATE": "DD MMM YYYY", + "DATETIME": "DD MMM YYYY HH:mm", + "SAVE": "Zapisz", + "CANCEL": "Anuluj", + "ACCEPT": "Akceptuj", + "DELETE": "Usuń", + "CREATE": "Stwórz", + "ADD": "Dodaj", + "COPY_TO_CLIPBOARD": "Skopiuj do schowka: Ctrl+C", + "EDIT": "Edycja", + "DRAG": "Przeciągnij", + "TAG_LINE": "Twoje zwinne, wolne, otwartoźródłowe narzędzie do zarządzania projektem", + "TAG_LINE_2": "Pokochaj swój projekt!", + "BLOCK": "Blokuj", + "UNBLOCK": "Odblokuj", + "BLOCKED": "Zablokowane", + "CREATED_BY": "Utworzone przez {{fullDisplayName}}", + "FROM": "od", + "TO": "do", + "CLOSE": "zamknij", + "BLOCKED_NOTE": "Dlaczego ta historyjka użytkownika jest zablokowana?", + "BLOCKED_REASON": "Wyjaśnij powód", + "GO_HOME": "Zabierz mnie do strony domowej", + "PLUGINS": "Wtyczki", + "BETA": "Wersja beta", + "ONE_ITEM_LINE": "Jedna pozycja na wiersz...", + "NEW_BULK": "Nowe zbiorcze dodawanie", + "RELATED_TASKS": "Zadania pokrewne", + "LOGOUT": "Wyloguj", + "EXTERNAL_USER": "zewnętrzny użytkownik", + "GENERIC_ERROR": "Umpa Lumpa mówi {{error}}.", + "IOCAINE_TEXT": "Czujesz się trochę przytłoczony zadaniem? Daj znać innym klikając na ikonę Iokainy podczas edycji zadania. Jest szansa, że staniesz się odporny na tą (fikcyjną ;) ) śmiertelną truciznę biorąc małe dawki. Z pewnością jednak da Ci ona dodatkowego kopa, który pomoże w pokonaniu nowego wyzwania i staniu się lepszym w tym co robisz!", + "CAPSLOCK_WARNING": "UWAGA! Klawisz CAPSLOCK jest aktywny.", + "FORM_ERRORS": { + "DEFAULT_MESSAGE": "Nieprawidłowa wartość", + "TYPE_EMAIL": "Podaj prawidłowy adres email.", + "TYPE_URL": "Podaj prawidłowy link.", + "TYPE_URLSTRICT": "Tutaj powinien być prawidłowy link.", + "TYPE_NUMBER": "Podaj prawidłową wartość liczbową.", + "TYPE_DIGITS": "Tutaj powinny znajdować się cyfry.", + "TYPE_DATEISO": "Tutaj powinna znajdować się data w formacie (RRRR-MM-DD).", + "TYPE_ALPHANUM": "Tutaj powinna znajdować się wartość alfanumeryczna.", + "TYPE_PHONE": "Wpisz numer telefonu.", + "NOTNULL": "Ta wartość nie powinna być zerowa.", + "NOT_BLANK": "To pole nie powinno zostać puste.", + "REQUIRED": "To pole jest wymagane.", + "REGEXP": "Nieprawidłowa wartość", + "MIN": "Ta wartość powinna być większa lub równa od %s.", + "MAX": "Ta wartość powinna być mniejsza lub równa od %s.", + "RANGE": "Ta wartość powinna mieścić się w przedziale od %s do %s.", + "MIN_LENGTH": "Zbyt mało znaków. Wpisz %s lub więcej znaków.", + "MAX_LENGTH": "Zbyt dużo znaków. Wpisz %s lub mniej znaków.", + "RANGE_LENGTH": "Niewłaściwa ilość znaków. Wpisz pomiędzy %s a %s znaków.", + "MIN_CHECK": "Musisz zaznaczyć przynajmniej %s pozycji.", + "MAX_CHECK": "Musisz zaznaczyć %s pozycji lub mniej.", + "RANGE_CHECK": "Musisz zaznaczyć pomiędzy %s a %s pozycji.", + "EQUAL_TO": "Ta wartość powinna być taka sama." + }, + "PICKERDATE": { + "FORMAT": "DD MMM YYYY", + "IS_RTL": "false", + "FIRST_DAY_OF_WEEK": "1", + "PREV_MONTH": "Poprzedni Miesiąc", + "NEXT_MONTH": "Następny Miesiąc", + "MONTHS": { + "JAN": "Styczeń", + "FEB": "Luty", + "MAR": "Marzec", + "APR": "Kwiecień", + "MAY": "Maj", + "JUN": "Czerwiec", + "JUL": "Lipiec", + "AUG": "Sierpień", + "SEP": "Wrzesień", + "OCT": "Październik", + "NOV": "Listopad", + "DEC": "Grudzień" + }, + "WEEK_DAYS": { + "SUN": "Niedziela", + "MON": "Poniedziałek", + "TUE": "Wtorek", + "WED": "Środa", + "THU": "Czwartek", + "FRI": "Piątek", + "SAT": "Sobota" + }, + "WEEK_DAYS_SHORT": { + "SUN": "Nd", + "MON": "Pon", + "TUE": "Wt", + "WED": "Śr", + "THU": "Czw", + "FRI": "Pt", + "SAT": "Sob" + } + }, + "SEE_USER_PROFILE": "Zobacz profil użytkownika {{username }}", + "USER_STORY": "Historyjka użytkownika", + "TASK": "Zadania", + "ISSUE": "Zgłoszenie", + "TAGS": { + "PLACEHOLDER": "Otaguj mnie!...", + "DELETE": "Usuń tag", + "ADD": "Dodaj tag" + }, + "DESCRIPTION": { + "EMPTY": "Puste miejsce jest takie nudne... opisz, przydaj sensu...", + "NO_DESCRIPTION": "Jeszcze bez opisu" + }, + "FIELDS": { + "SUBJECT": "Temat", + "NAME": "Nazwa", + "URL": "Link", + "DESCRIPTION": "Opis", + "VALUE": "Wartość", + "SLUG": "Slug", + "COLOR": "Kolor", + "IS_CLOSED": "Zamknięte?", + "STATUS": "Status", + "TYPE": "Typ", + "SEVERITY": "Ważność", + "PRIORITY": "Priorytet", + "ASSIGNED_TO": "Przydzielone do", + "POINTS": "Punkty", + "BLOCKED_NOTE": "zablokowana notka", + "IS_BLOCKED": "zablokowana", + "REF": "Ref", + "VOTES": "Głosy" + }, + "ROLES": { + "ALL": "Wszystko" + }, + "ASSIGNED_TO": { + "NOT_ASSIGNED": "Nieprzypisane", + "DELETE_ASSIGNMENT": "Usuń przypisanie", + "REMOVE_ASSIGNED": "Ukryj przypisane", + "TOO_MANY": "...zbyt wielu użytkowników, filtruj dalej Umpa Lumpy nie ogarniają", + "CONFIRM_UNASSIGNED": "Jesteś pewny, że chcesz pozostawić nieprzypisane?", + "TITLE_ACTION_EDIT_ASSIGNMENT": "Edytuj przypisanie" + }, + "STATUS": { + "CLOSED": "Zamknięte", + "OPEN": "Otwórz" + }, + "WATCHERS": { + "ADD": "Dodaj obserwatorów", + "TITLE_ADD": "Dodaj członka projektu do listy obserwatorów.", + "DELETE": "Usuń obserwatora", + "TITLE_LIGHTBOX_DELETE_WARTCHER": "Usuwanie obserwatora..." + }, + "WATCH_BUTTON": { + "WATCH": "Obserwuj", + "WATCHING": "Obserwujesz", + "UNWATCH": "Nie obserwuj", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Obserwuj lub przestań obserwować", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Oceń pozytywnie", + "UPVOTED": "Ocenione pozytywnie", + "DOWNVOTE": "Oceń negatywnie", + "VOTERS": "Voters", + "BUTTON_TITLE": "Ocen ten obiekt", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, + "CUSTOM_ATTRIBUTES": { + "CUSTOM_FIELDS": "Własne Pola", + "SAVE": "Zapisz pole niestandardowe", + "EDIT": "Edytuj pole niestandardowe", + "DELETE": "Usuń niestandardowy atrybut", + "CONFIRM_DELETE": "Pamiętaj, że wszystkie wartości w tym polu zostaną usunięte.
Czy kontynuować?" + }, + "FILTERS": { + "TITLE": "filtry", + "INPUT_PLACEHOLDER": "Temat lub odniesienie", + "TITLE_ACTION_FILTER_BUTTON": "szukaj", + "BREADCRUMB_TITLE": "wróć do kategorii", + "BREADCRUMB_FILTERS": "Filtry", + "BREADCRUMB_STATUS": "status" + }, + "WYSIWYG": { + "H1_BUTTON": "Nagłówek pierwszego poziomu", + "H1_SAMPLE_TEXT": "Przykładowy tekst...", + "H2_BUTTON": "Nagłówek drugiego poziomu", + "H2_SAMPLE_TEXT": "Przykładowy tekst...", + "H3_BUTTON": "Nagłówek trzeciego poziomu", + "H3_SAMPLE_TEXT": "Przykładowy tekst...", + "BOLD_BUTTON": "Pogrubienie", + "BOLD_BUTTON_SAMPLE_TEXT": "Przykładowy tekst...", + "ITALIC_BUTTON": "Kursywa", + "ITALIC_SAMPLE_TEXT": "Przykładowy tekst...", + "STRIKE_BUTTON": "Przekreślenie", + "STRIKE_SAMPLE_TEXT": "Przykładowy tekst...", + "BULLETED_LIST_BUTTON": "Lista", + "BULLETED_LIST_SAMPLE_TEXT": "Przykładowy tekst...", + "NUMERIC_LIST_BUTTON": "Lista Numerowana", + "NUMERIC_LIST_SAMPLE_TEXT": "Przykładowy tekst...", + "PICTURE_BUTTON": "Obraz", + "PICTURE_SAMPLE_TEXT": "Tekst alternatywny dla obrazka...", + "LINK_BUTTON": "Link", + "LINK_SAMPLE_TEXT": "Twój tekst do linku...", + "QUOTE_BLOCK_BUTTON": "Cytat", + "QUOTE_BLOCK_SAMPLE_TEXT": "Przykładowy tekst...", + "CODE_BLOCK_BUTTON": "Blok Kodu", + "CODE_BLOCK_SAMPLE_TEXT": "Przykładowy tekst...", + "PREVIEW_BUTTON": "Podgląd", + "EDIT_BUTTON": "Edycja", + "MARKDOWN_HELP": "Składnia Markdown pomoc" + }, + "PERMISIONS_CATEGORIES": { + "SPRINTS": { + "NAME": "Sprinty", + "VIEW_SPRINTS": "Przeglądaj Sprinty", + "ADD_SPRINTS": "Dodawaj sprinty", + "MODIFY_SPRINTS": "Modyfikuj sprinty", + "DELETE_SPRINTS": "Usuwaj sprinty" + }, + "USER_STORIES": { + "NAME": "Historyjki użytkownika", + "VIEW_USER_STORIES": "Przeglądaj historyjki użytkownika", + "ADD_USER_STORIES": "Dodawaj historyjki użytkownika", + "MODIFY_USER_STORIES": "Modyfikuj historyjki użytkownika", + "DELETE_USER_STORIES": "Usuwaj historyjki użytkownika" + }, + "TASKS": { + "NAME": "Zadania", + "VIEW_TASKS": "Przeglądaj zadania", + "ADD_TASKS": "Dodawaj zadania", + "MODIFY_TASKS": "Modyfikuj zadania", + "DELETE_TASKS": "Usuwaj zadania" + }, + "ISSUES": { + "NAME": "Zgłoszenia", + "VIEW_ISSUES": "Przeglądaj zgłoszenia", + "ADD_ISSUES": "Dodawaj zgłoszenia", + "MODIFY_ISSUES": "Modyfikuj zgłoszenia", + "DELETE_ISSUES": "Usuwaj zgłoszenia" + }, + "WIKI": { + "NAME": "Wiki", + "VIEW_WIKI_PAGES": "Przeglądaj strony Wiki", + "ADD_WIKI_PAGES": "Dodawaj strony Wiki", + "MODIFY_WIKI_PAGES": "Modyfikuj strony Wiki", + "DELETE_WIKI_PAGES": "Usuwaj strony Wiki", + "VIEW_WIKI_LINKS": "Przeglądaj linki Wiki", + "ADD_WIKI_LINKS": "Dodawaj linki Wiki", + "DELETE_WIKI_LINKS": "Usuwaj linki Wiki" + } + }, + "META": { + "PAGE_TITLE": "Taiga", + "PAGE_DESCRIPTION": "Taiga to platforma do zarządzania projektami dla startup'ów i zwinnych deweloperów i projektantów, którzy potrzebują prostego,atrakcyjnego wizualnie narzędzia. Taiga sprawia, że praca z projektem staje się przyjemniejsza." + } + }, + "LOGIN": { + "PAGE_TITLE": "Logowanie - Taiga", + "PAGE_DESCRIPTION": "Logowanie do Taiga, platformy do zarządzania projektami dla startup'ów oraz zwinnych developerów i designerów, którzy potrzebują prostego, ładnego narzędzia, które sprawi, że praca stanie się przyjemnością." + }, + "AUTH": { + "INVITED_YOU": "zaprasza cię do dołączenia do projektu", + "NOT_REGISTERED_YET": "Jeszcze nie zarejestrowany?", + "REGISTER": "Zarejestruj", + "CREATE_ACCOUNT": "Tutaj utwórz swoje darmowe konto" + }, + "LOGIN_COMMON": { + "HEADER": "Mam już login do Taigi", + "PLACEHOLDER_AUTH_NAME": "Login albo e-mail (uwzględnij wielkość liter)", + "LINK_FORGOT_PASSWORD": "Zapomniałeś?", + "TITLE_LINK_FORGOT_PASSWORD": "Zapomniałeś hasła?", + "ACTION_ENTER": "Wprowadź", + "ACTION_SIGN_IN": "Zaloguj", + "PLACEHOLDER_AUTH_PASSWORD": "Hasło (uwzględnij wielkość liter)" + }, + "LOGIN_FORM": { + "ERROR_AUTH_INCORRECT": "Nasze Umpa Lumpy twierdzą,że Twój login/e-mail lub hasło nie są poprawne.", + "SUCCESS": "Nasze Umpa Lumpy są szczęśliwe, witaj w Taiga." + }, + "REGISTER": { + "PAGE_TITLE": "Rejestracja - Taiga", + "PAGE_DESCRIPTION": "Załóż własne konto w Taiga, platformie do zarządzania projektami dla startup'ów i zwinnych deweloperów oraz designerów, którzy chcą prostego, pięknego narzędzia sprawiającego, że praca jest przyjemna." + }, + "REGISTER_FORM": { + "TITLE": "Utwórz nowe konto (bezpłatnie)", + "PLACEHOLDER_NAME": "Wybierz nazwę użytkownika (z uwzględnieniem wielkości liter)", + "PLACEHOLDER_FULL_NAME": "Podaj twoje imię i nazwisko", + "PLACEHOLDER_EMAIL": "Twój e-mail", + "PLACEHOLDER_PASSWORD": "Wpisz nowe hasło (z uwzględnieniem wielkości liter)", + "ACTION_SIGN_UP": "Zarejestruj", + "TITLE_LINK_LOGIN": "Zaloguj", + "LINK_LOGIN": "Jesteś już zarejestrowany? Zaloguj się" + }, + "FORGOT_PASSWORD": { + "PAGE_TITLE": "Zapomniałem hasła - Taiga", + "PAGE_DESCRIPTION": "Podaj twoją nazwę użytkownika lub e-mail by zdobyć nowe hasło i odzyskać dostęp do Taiga." + }, + "FORGOT_PASSWORD_FORM": { + "TITLE": "Ups, czyżbyś zapomniał hasło?", + "SUBTITLE": "Podaj twoją nazwę użytkownika lub e-mail by zdobyć nowe", + "PLACEHOLDER_FIELD": "Login albo e-mail", + "ACTION_RESET_PASSWORD": "Resetuj hasło", + "LINK_CANCEL": "Nie, zabierz mnie stąd. Chyba je pamiętam.", + "SUCCESS": "Sprawdź swoją skrzynkę!
Wysłaliśmy Ci wiadomość e-mail z instrukcją jak ustawić nowe hasło.", + "ERROR": "Nasze Umpa Lumpy twierdzą, że nie jesteś jeszcze zarejestrowany." + }, + "CHANGE_PASSWORD": { + "PAGE_TITLE": "Zmień hasło - Taiga", + "PAGE_DESCRIPTION": "Ustaw nowe hasło dla swojego konta Taiga. Ej! może byś coś zjadł? To jest dobre dla Twojego mózgu :)", + "SECTION_NAME": "Zmień hasło", + "FIELD_CURRENT_PASSWORD": "Obecne hasło", + "PLACEHOLDER_CURRENT_PASSWORD": "Twoje obecne hasło (lub puste, jeśli jeszcze go nie masz)", + "FIELD_NEW_PASSWORD": "Nowe hasło", + "PLACEHOLDER_NEW_PASSWORD": "Wpisz nowe hasło", + "FIELD_RETYPE_PASSWORD": "Wpisz ponownie nowe hasło", + "PLACEHOLDER_RETYPE_PASSWORD": "Wpisz ponownie nowe hasło", + "ERROR_PASSWORD_MATCH": "Hasło nie jest zgodne" + }, + "CHANGE_PASSWORD_RECOVERY_FORM": { + "TITLE": "Ustaw nowe hasło do Taiga", + "SUBTITLE": "Powinieneś spróbować jeść więcej rzeczy bogatych w żelazo, są dobre dla Twojej pamięci :P", + "PLACEHOLDER_NEW_PASSWORD": "Nowe hasło", + "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Wpisz ponownie nowe hasło", + "ACTION_RESET_PASSWORD": "Resetuj hasło", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", + "SUCCESS": "Nasze Umpa Lumpy zapisały Twoje nowe hasło
Spróbuj zalogować się z jego pomocą." + }, + "INVITATION": { + "PAGE_TITLE": "Akceptowanie zaproszenia - Taiga", + "PAGE_DESCRIPTION": "Zaakceptuj zaproszenie, aby dołączyć do projektu w Taiga, platformie do zarządzania projektami dla startup'ów i zwinnych deweloperów oraz designerów, którzy chcą prostego, pięknego narzędzia sprawiającego, że praca jest przyjemna." + }, + "INVITATION_LOGIN_FORM": { + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", + "SUCCESS": "Udało Ci się dołączyć do tego projektu. Witaj w {{project_name}}", + "ERROR": "Według naszych Umpa Lump, nie jesteś jeszcze zarejestrowany. Albo wpisałeś złe hasło." + }, + "HOME": { + "PAGE_TITLE": "Strona główna - Taiga", + "PAGE_DESCRIPTION": "Główna strona Taiga, z Twoimi głównymi projektami i wszystkimi przypisanymi Tobie i obserwowanymi historyjkami użytkownika, zadaniami i zgłoszeniami.", + "EMPTY_WORKING_ON": "Trochę tu pusto?Rozpocznij pracę z Taigą, a pojawią się tutaj historie, zadania oraz zgłoszone błędy, nad którymi pracujesz.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", + "EMPTY_PROJECT_LIST": "Nie masz jeszcze żadnych projektów", + "WORKING_ON_SECTION": "Pracujesz nad", + "WATCHING_SECTION": "Obserwujesz" + }, + "PROJECTS": { + "PAGE_TITLE": "Moje projekty - Taiga", + "PAGE_DESCRIPTION": "Lista wszystkich Twoich projektów, możesz zmieniać ich kolejność lub tworzyć nowe.", + "MY_PROJECTS": "Moje projekty" + }, + "ATTACHMENT": { + "SECTION_NAME": "załączniki", + "TITLE": "{{ plik }} załadowany dnia {{ data }}", + "DESCRIPTION": "Wpisz krótki opis", + "DEPRECATED": "(przestarzały)", + "DEPRECATED_FILE": "Przestarzałe?", + "ADD": "Dodaj nowy załącznik. {{maxFileSizeMsg}}", + "MAX_FILE_SIZE": "[Maks. rozmiar: {{maxFileSize}}]", + "SHOW_DEPRECATED": "+ pokaż przestarzałe załączniki", + "HIDE_DEPRECATED": "- ukryj przestarzałe załączniki", + "COUNT_DEPRECATED": "({{ counter }} przestarzałych", + "MAX_UPLOAD_SIZE": "Maksymalny rozmiar wysyłania to {{maxFileSize}}", + "DATE": "DD MMM YYYY [at] hh:mm", + "ERROR_UPLOAD_ATTACHMENT": "Nie udało się przesłać pliku '{{fileName}}'. {{errorMessage}}", + "TITLE_LIGHTBOX_DELETE_ATTACHMENT": "Usuń załącznik...", + "MSG_LIGHTBOX_DELETE_ATTACHMENT": "załącznik '{{fileName}}'", + "ERROR_DELETE_ATTACHMENT": "Nie udało się usunąć załącznika w związku z następującym błędem: {{errorMessage}}", + "FIELDS": { + "IS_DEPRECATED": "jest przedawniony" + } + }, + "PAGINATION": { + "PREVIOUS": "Poprzedni", + "NEXT": "Następny" + }, + "ADMIN": { + "COMMON": { + "TITLE_ACTION_EDIT_VALUE": "Edytuj wartość", + "TITLE_ACTION_DELETE_VALUE": "Usuń wartość" + }, + "HELP": "Potrzebujesz pomocy? Sprawdź naszą stronę wsparcia!", + "PROJECT_DEFAULT_VALUES": { + "TITLE": "Domyślne wartości", + "SUBTITLE": "Ustaw wartości domyślne dla wszystkich pól wyboru." + }, + "MEMBERSHIPS": { + "TITLE": "Zarządzaj członkami", + "PAGE_TITLE": "Członkostwa - {{projectName}}", + "ADD_BUTTON": "+ Nowy członek", + "ADD_BUTTON_TITLE": "Dodaj nowego członka" + }, + "PROJECT_EXPORT": { + "TITLE": "Eksport", + "SUBTITLE": "Wyeksportuj twój projekt aby utworzyć kopię zapasową lub stworzyć nowy bazujący na nim.", + "EXPORT_BUTTON": "Eksport", + "EXPORT_BUTTON_TITLE": "Eksport projektu", + "LOADING_TITLE": "Generujemy plik zrzutu", + "DUMP_READY": "Twój plik zrzutu jest gotowy!", + "LOADING_MESSAGE": "Proszę, nie zamykaj tej strony.", + "ASYNC_MESSAGE": "Wyślemy Ci wiadomość e-mail, kiedy będziemy gotowi.", + "SYNC_MESSAGE": "Jeśli pobieranie nie rozpocznie się automatycznie kliknij tutaj.", + "ERROR": "Umpa Lumpy mają problem z wygenerowaniem zrzutu. Spróbuj ponownie.", + "ERROR_BUSY": "Przepraszam, Umpa Lumpy są teraz bardzo zajęte, spróbuj ponownie za chwilę.", + "ERROR_MESSAGE": "Umpa Lumpy mają następujący problem z wygenerowaniem zrzutu: {{message}}" + }, + "MODULES": { + "TITLE": "Moduły", + "ENABLE": "Włącz", + "DISABLE": "Wyłącz", + "BACKLOG": "Dziennik", + "BACKLOG_DESCRIPTION": "Zarządzaj swoimi historyjkami użytkownika aby utrzymać zorganizowany widok i priorytety zadań", + "KANBAN": "Kanban", + "KANBAN_DESCRIPTION": "Organizuj swój projekt przy użyciu metody lean.", + "ISSUES": "Zgłoszenia", + "ISSUES_DESCRIPTION": "Śledź błędy, pytania i ulepszenia związane z projektem. Nie przegap niczego!", + "WIKI": "Wiki", + "WIKI_DESCRIPTION": "Dodawaj, modyfikuj lub usuwaj dane we współpracy z innymi. To jest właściwe miejsce dla Twojej dokumentacji projektowej.", + "MEETUP": "Spotkaj się", + "MEETUP_DESCRIPTION": "Wybierz system wideokonferencji. Nawet programiści potrzebują kontaktu twarzą w twarz.", + "SELECT_VIDEOCONFERENCE": "Wybierz system do wideokonferencji.", + "SALT_CHAT_ROOM": "Jeśli chcesz możesz dodać salt code do nazwy pokoju chatu.", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Niestandardowy", + "URL_CHAT_ROOM": "Link do chat roomu" + }, + "PROJECT_PROFILE": { + "PAGE_TITLE": "{{sectionName}} - Profil projektu - {{projectName}}", + "PROJECT_DETAILS": "Szczegóły projektu", + "PROJECT_NAME": "Nazwa projektu", + "PROJECT_SLUG": "Szczegóły projektu", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", + "TAGS": "Tagi", + "DESCRIPTION": "Opis", + "PUBLIC_PROJECT": "Projekt publiczny", + "PRIVATE_PROJECT": "Projekt prywatny", + "DELETE": "Usuń ten projekt" + }, + "REPORTS": { + "TITLE": "Raporty", + "SUBTITLE": "Wyeksportuj dane projektu w CSV i stwórz swoje własne projekty", + "DESCRIPTION": "Pobierz plik CSV lub skopiuj wygenerowany adres URL i otwórz go w edytorze tekstu lub arkuszu kalkulacyjnym, aby tworzyć własne raporty danych projektu. Będziesz mógł łatwo wizualizować i analizować wszystkie dane.", + "HELP": "Jak mogę tego użyć we własnym arkuszu kalkulacyjnym?", + "REGENERATE_TITLE": "Zmień link", + "REGENERATE_SUBTITLE": "Zamierzasz zmienić link dostępu do danych CSV. Poprzedni link będzie niedostępny. Czy jesteś pewien?" + }, + "CSV": { + "SECTION_TITLE_US": "raporty historii użytkownika", + "SECTION_TITLE_TASK": "Raporty zadań", + "SECTION_TITLE_ISSUE": "raporty zgłoszeń", + "DOWNLOAD": "Ściągnij CSV", + "URL_FIELD_PLACEHOLDER": "Proszę wygeneruj ponownie link do CSV", + "TITLE_REGENERATE_URL": "Wygeneruj ponownie link do CSV", + "ACTION_GENERATE_URL": "Wygeneruj link", + "ACTION_REGENERATE": "Wygeneruj ponownie" + }, + "CUSTOM_FIELDS": { + "TITLE": "Własne Pola", + "SUBTITLE": "Zdefiniuj własne dodatkowe pola dla historyjek użytkownika, zadań i zgłoszeń.", + "US_DESCRIPTION": "Własne pola dla historyjek użytkownika", + "US_ADD": "Dodaj własne pole dla historyjek użytkownika", + "TASK_DESCRIPTION": "Własne pola dla zadań", + "TASK_ADD": "Dodaj Własne pole dla zadań", + "ISSUE_DESCRIPTION": "Własne pola dla zgłoszeń", + "ISSUE_ADD": "Dodaj własne pole dla zgłoszeń", + "FIELD_TYPE_TEXT": "Tekst", + "FIELD_TYPE_MULTI": "Pole wielowierszowe", + "FIELD_TYPE_DATE": "Date" + }, + "PROJECT_VALUES": { + "PAGE_TITLE": "{{sectionName}} - Wartości projektu - {{projectName}}", + "REPLACEMENT": "Wszystkie wpisy z tą wartością zostaną zmienione na", + "ERROR_DELETE_ALL": "Nie możesz usunąć wszystkich wartości." + }, + "PROJECT_VALUES_POINTS": { + "TITLE": "Punkty", + "SUBTITLE": "Podaj możliwe estymaty dla historyjek użytkownika", + "US_TITLE": "punkty", + "ACTION_ADD": "Dodaj nową wartość" + }, + "PROJECT_VALUES_PRIORITIES": { + "TITLE": "Priorytety", + "SUBTITLE": "Zdefiniuj priorytety dla zgłoszeń", + "ISSUE_TITLE": "Priorytety zgłoszeń", + "ACTION_ADD": "Dodaj nowy priorytet" + }, + "PROJECT_VALUES_SEVERITIES": { + "TITLE": "Ważność", + "SUBTITLE": "Zdefiniuj ważność dla zgłoszeń", + "ISSUE_TITLE": "Ważność zgłoszeń", + "ACTION_ADD": "Dodaj nową ważność" + }, + "PROJECT_VALUES_STATUS": { + "TITLE": "Status", + "SUBTITLE": "Zdefiniuj statusy dla historyjek użytkownika, zadań i zgłoszeń.", + "US_TITLE": "Statusy", + "TASK_TITLE": "Statusy zadań", + "ISSUE_TITLE": "Statusy zgłoszeń" + }, + "PROJECT_VALUES_TYPES": { + "TITLE": "Typy", + "SUBTITLE": "Zdefiniuj typy zgłoszeń", + "ISSUE_TITLE": "Typy zgłoszeń", + "ACTION_ADD": "Dodaj nowy {{objName}}" + }, + "ROLES": { + "PAGE_TITLE": "Role - {{projectName}}", + "WARNING_NO_ROLE": "Bez przydzielenia ról w projekcie nie ma możliwości oceniania historyjek użytkownika. Umpa Lumpy nie będą wiedziały komu wolno to zrobić :)", + "HELP_ROLE_ENABLED": "Jeśli aktywne użytkownicy pełniący tę rolę będą mogli szacować wartości historyjek", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", + "COUNT_MEMBERS": "{{ role.members_count }} użytkowników pełniących tę rolę w projekcie", + "TITLE_DELETE_ROLE": "Usuń rolę", + "REPLACEMENT_ROLE": "Wszyscy użytkownicy pełniący tę rolę zostaną przeniesieni do", + "WARNING_DELETE_ROLE": "Ostrożnie, wszystkie estymaty dodane przez ludzi pełniących tę rolę zostaną usunięte", + "ERROR_DELETE_ALL": "Nie możesz usunąć wszystkich wartości", + "EXTERNAL_USER": "Zewnętrzny użytkownik" + }, + "THIRD_PARTIES": { + "SECRET_KEY": "Tajny klucz", + "PAYLOAD_URL": "Payload URL", + "VALID_IPS": "Adresy IP(oddzielone przecinkami)" + }, + "BITBUCKET": { + "SECTION_NAME": "Bitbucket", + "PAGE_TITLE": "Bitbucket - {{projectName}}", + "INFO_VERIFYING_IP": "Zapytania Bitbucket nie są szyfrowane zatem najlepszym sposobem weryfikacji źródła jest sprawdzenie adresu IP.Jeśli pole pozostanie puste sprawdzanie IP nie będzie działać." + }, + "GITLAB": { + "SECTION_NAME": "Gitlab", + "PAGE_TITLE": "Gitlab - {{projectName}}", + "INFO_VERIFYING_IP": "Zapytania Gitlab nie są szyfrowane zatem najlepszym sposobem weryfikacji źródła jest sprawdzenie adresu IP.Jeśli pole pozostanie puste sprawdzanie IP nie będzie działać." + }, + "GITHUB": { + "SECTION_NAME": "Github", + "PAGE_TITLE": "Github - {{projectName}}" + }, + "WEBHOOKS": { + "PAGE_TITLE": "Webhooks - {{projectName}}", + "SECTION_NAME": "Webhooks", + "SUBTITLE": "Webhooks notify external services about events in Taiga, like comments, user stories....", + "ADD_NEW": "Dodaj nowy webhook", + "TYPE_NAME": "Wpisz nazwę serwisu", + "TYPE_PAYLOAD_URL": "Type the service payload url", + "TYPE_SERVICE_SECRET": "Wpisz sekretny klucz", + "SAVE": "Zapisz Webhook", + "CANCEL": "Cancel Webhook", + "SHOW_HISTORY": "(Show history)", + "TEST": "Testuj Webhook", + "EDIT": "Zmień Webhook", + "DELETE": "Usuń Webhook", + "REQUEST": "Żądanie", + "RESEND_REQUEST": "Resend request", + "HEADERS": "Headers", + "PAYLOAD": "Payload", + "RESPONSE": "Response", + "DATE": "DD MMM YYYY [at] hh:mm", + "ACTION_HIDE_HISTORY": "(Ukryj historię)", + "ACTION_HIDE_HISTORY_TITLE": "Ukryj szczegóły historii", + "ACTION_SHOW_HISTORY": "(Pokaż historię)", + "ACTION_SHOW_HISTORY_TITLE": "Pokaż szczegóły historii", + "WEBHOOK_NAME": "Webhook '{{name}}'" + }, + "CUSTOM_ATTRIBUTES": { + "PAGE_TITLE": "{{sectionName}} - Niestandardowe atrybuty - {{projectName}}", + "ADD": "Dodaj niestandardowe pole", + "EDIT": "Edytuj niestandardowe pole", + "DELETE": "Usuń niestandardowe pole", + "SAVE_TITLE": "Zapisz niestandardowe pole", + "CANCEL_TITLE": "Anuluj tworzenie", + "SET_FIELD_NAME": "podaj nazwę pola", + "SET_FIELD_DESCRIPTION": "podaj opis pola", + "ACTION_UPDATE": "Aktualizuj pole", + "ACTION_CANCEL_EDITION": "Anuluj edycję" + }, + "MEMBERSHIP": { + "COLUMN_MEMBER": "Członek", + "COLUMN_ADMIN": "Admin", + "COLUMN_ROLE": "Rola", + "COLUMN_STATUS": "Status", + "STATUS_ACTIVE": "Aktywny", + "STATUS_PENDING": "Nieaktywny", + "DELETE_MEMBER": "Usuń członka", + "RESEND": "Resend", + "SUCCESS_SEND_INVITATION": "Ponownie wysłano zaproszenie do '{{e-mail}}'.", + "ERROR_SEND_INVITATION": "Zaproszenie nie zostało wysłane.", + "SUCCESS_DELETE": "Usunięto {{message}}.", + "ERROR_DELETE": "Nie usunięto {{message}}.", + "DEFAULT_DELETE_MESSAGE": "zaproszenie do {{e-mail}}" + }, + "DEFAULT_VALUES": { + "LABEL_POINTS": "Domyślna wartość dla selektora punktów", + "LABEL_US": "Domyślna wartość dla selektora statusu historyjek użytkownika", + "LABEL_TASK_STATUS": "Domyśla wartość dla selektora statusu zadań", + "LABEL_PRIORITY": "Domyślna wartość dla selektora priorytetu", + "LABEL_SEVERITY": "Domyślna wartość dla selektora ważności", + "LABEL_ISSUE_TYPE": "Domyślna wartość dla selektora typu zgłoszenia", + "LABEL_ISSUE_STATUS": "Domyślna wartość dla selektora statusu zgłoszenia" + }, + "STATUS": { + "PLACEHOLDER_WRITE_STATUS_NAME": "Wpisz nazwę nowego statusu" + }, + "TYPES": { + "PLACEHOLDER_WRITE_NAME": "Wpisz nazwę nowego elementu" + }, + "US_STATUS": { + "ACTION_ADD_STATUS": "Dodaj nowy status", + "IS_ARCHIVED_COLUMN": "Zarchiwizowana?", + "WIP_LIMIT_COLUMN": "WIP Limit", + "PLACEHOLDER_WRITE_NAME": "Wpisz nazwę nowego statusu" + }, + "MENU": { + "TITLE": "Admin", + "PROJECT": "Projekt", + "ATTRIBUTES": "Atrybuty", + "MEMBERS": "Członkowie", + "PERMISSIONS": "Uprawnienia", + "INTEGRATIONS": "Integracje", + "PLUGINS": "Wtyczki" + }, + "SUBMENU_PROJECT_ATTRIBUTES": { + "TITLE": "Atrybuty" + }, + "SUBMENU_PROJECT_VALUES": { + "STATUS": "Status", + "POINTS": "Punkty", + "PRIORITIES": "Priorytety", + "SEVERITIES": "Ważność", + "TYPES": "Typy", + "CUSTOM_FIELDS": "Niestandardowe pola" + }, + "SUBMENU_PROJECT_PROFILE": { + "TITLE": "Profil projektu" + }, + "SUBMENU_ROLES": { + "TITLE": "Role", + "ACTION_NEW_ROLE": "+ Nowa rola", + "TITLE_ACTION_NEW_ROLE": "Dodaj nową rolę" + }, + "SUBMENU_THIDPARTIES": { + "TITLE": "Ważność" + } + }, + "USER": { + "PROFILE": { + "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", + "EDIT": "Edycja profilu", + "FOLLOW": "Obserwuj", + "CLOSED_US": "Zamknięte historyjki użytkownika", + "PROJECTS": "Projekty", + "PROJECTS_EMPTY": "{{username}} jeszcze nie uczestniczy w żadnym projekcie", + "CONTACTS": "Kontakty", + "CONTACTS_EMPTY": "{{username}} jeszcze nie ma kontaktów", + "CURRENT_USER_CONTACTS_EMPTY": "Jeszcze nie masz kontaktów", + "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Ludzie z którymi pracujesz w Taiga staną się Twoimi kontaktami automatycznie.", + "REPORT": "Zgłoś naruszenie", + "TABS": { + "ACTIVITY_TAB": "Aktywność", + "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "PROJECTS_TAB": "Projekty", + "PROJECTS_TAB_TITLE": "List all projects that this user is a member", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Głosy", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Obserwowane", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Kontakty", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } + }, + "PROFILE_SIDEBAR": { + "TITLE": "Twój profil", + "DESCRIPTION": "Użytkownicy mogą zobaczyć wszystko co robisz i nad czym pracujesz. Dodaj jakieś ładne info o sobie, żeby dać im się poznać.", + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Type something...", + "FILTER_TYPE_ALL": "Wszystkie", + "FILTER_TYPE_ALL_TITLE": "Show all", + "FILTER_TYPE_PROJECTS": "Projekty", + "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", + "FILTER_TYPE_TASKS": "Zadania", + "FILTER_TYPE_TASK_TITLES": "Show only tasks", + "FILTER_TYPE_ISSUES": "Zgłoszenia", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." + } + }, + "PROJECT": { + "PAGE_TITLE": "{{projectName}}", + "WELCOME": "Witamy", + "SECTION_PROJECTS": "Projekty", + "HELP": "Ustal kolejność Twoich projektów tak, aby na górze znalazły się te najważniejsze.
Pierwsze 10 projektów pojawi się w liście projektów na górnym pasku nawigacji.", + "PRIVATE": "Projekt prywatny", + "STATS": { + "PROJECT": "projekt
punkty", + "DEFINED": "zdefiniowane
punkty", + "ASSIGNED": "przypisane
punkty", + "CLOSED": "zamknięte
punkty" + }, + "SECTION": { + "SEARCH": "Szukaj", + "TIMELINE": "Oś czasu", + "BACKLOG": "Dziennik", + "KANBAN": "Kanban", + "ISSUES": "Zgłoszenia", + "WIKI": "Wiki", + "TEAM": "Zespół", + "MEETUP": "Spotkaj się", + "ADMIN": "Admin" + }, + "NAVIGATION": { + "SECTION_TITLE": "Twoje projekty", + "PLACEHOLDER_SEARCH": "Szukaj w ...", + "ACTION_CREATE_PROJECT": "Stwórz projekt", + "ACTION_IMPORT_PROJECT": "Importuj projekt", + "SEE_MORE_PROJECTS": "Zobacz więcej projektów", + "TITLE_CREATE_PROJECT": "Utwórz projekt", + "TITLE_IMPORT_PROJECT": "Importuj projekt", + "TITLE_PRVIOUS_PROJECT": "Pokaż poprzedni projekt", + "TITLE_NEXT_PROJECT": "Pokaż kolejne projekty", + "HELP_TITLE": "Taiga strona wsparcia", + "HELP": "Pomoc", + "FEEDBACK_TITLE": "Prześlij opinię", + "FEEDBACK": "Feedback", + "NOTIFICATIONS_TITLE": "Edytuj ustawienia powiadomień", + "NOTIFICATIONS": "Powiadomienia", + "ORGANIZATIONS_TITLE": "Edytuj organizacje", + "ORGANIZATIONS": "Edytuj organizacje", + "SETTINGS_TITLE": "Edytuj ustawienia", + "SETTINGS": "Ustawienia", + "VIEW_PROFILE_TITLE": "Zobacz profil", + "VIEW_PROFILE": "Zobacz profil", + "EDIT_PROFILE_TITLE": "Edytuj profil", + "EDIT_PROFILE": "Edytuj profil", + "CHANGE_PASSWORD_TITLE": "Zmień hasło", + "CHANGE_PASSWORD": "Zmień hasło", + "DASHBOARD_TITLE": "Dashboard", + "DISCOVER_TITLE": "Odkryj projekty", + "DISCOVER": "Odkryj", + "ACTION_REORDER": "Przeciągnij i upuść żeby zmienić kolejność" + }, + "IMPORT": { + "TITLE": "Importowanie Projektu", + "UPLOADING_FILE": "Umpa Lumpy ładują plik zrzutu", + "DESCRIPTION": "Proces może zająć chwilkę, proszę nie zamykaj okna.", + "ASYNC_IN_PROGRESS_TITLE": "Umpa Lumpy importują projekt", + "ASYNC_IN_PROGRESS_MESSAGE": "To może zając kilka minut.
Otrzymasz mail gdy wszystko będzie gotowe.", + "UPLOAD_IN_PROGRESS_MESSAGE": "Wysłano {{uploadedSize}} z {{totalSize}}", + "ERROR": "Umpa Lumpy mają problem z importem. Spróbuj ponownie.", + "ERROR_TOO_MANY_REQUEST": "Umpa Lumpy są teraz zajęte a serwer ma zadyszkę. Spróbuj ponownie za chwilę.", + "ERROR_MESSAGE": "Przy imporcie wystąpił następujący problem: {{error_message}}", + "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) plik jest za ciężki, maksymalna wartość to: ({{maxFileSize}})", + "SYNC_SUCCESS": "Twój projekt zaimportował się prawidłowo!" + }, + "LIKE_BUTTON": { + "LIKE": "Like", + "LIKED": "Liked", + "UNLIKE": "Unlike", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Watch", + "WATCHING": "Obserwujesz", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Receive all notifications", + "NOTIFY_ALL_TITLE": "Receive all notifications for this project", + "NOTIFY_INVOLVED": "Only involved", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Unwatch", + "UNWATCH_TITLE": "Unwatch this project" + } + } + }, + "LIGHTBOX": { + "DELETE_ACCOUNT": { + "SECTION_NAME": "Usuń konto z Taiga", + "CONFIRM": "Czy na pewno chcesz usunąć swoje konto z Taiga?", + "SUBTITLE": "Będziemy tęsknić! :(", + "NEWSLETTER_LABEL_TEXT": "Nie chcę więcej otrzymywać waszego newslettera" + }, + "DELETE_PROJECT": { + "TITLE": "Usuń projekt", + "QUESTION": "Czy masz pewność, że chcesz skasować ten projekt?", + "SUBTITLE": "Wszystkie dane projektu będą stracone! :(", + "CONFIRM": "Tak, jestem pewny" + }, + "ASSIGNED_TO": { + "SELECT": "Wybierz przypisane do", + "SEARCH": "Szukaj użytkowników" + }, + "ADD_MEMBER": { + "TITLE": "Nowy Członek", + "HELP_TEXT": "Jeżeli użytkownik jest już zarejestrowany w Taiga, będzie dodany automatycznie. W przeciwnym wypadku otrzyma zaproszenie." + }, + "CREATE_ISSUE": { + "TITLE": "Dodaj zgłoszenie" + }, + "FEEDBACK": { + "TITLE": "Powiedz nam coś...", + "COMMENT": "...błędy, sugestie, fajne propozycje..., nawet najgorsze koszmary z Taigi.", + "ACTION_SEND": "Prześlij opinię" + }, + "SEARCH": { + "TITLE": "Szukaj", + "PLACEHOLDER_SEARCH": "Czego szukasz?" + }, + "ADD_EDIT_SPRINT": { + "TITLE": "Nowy sprint", + "PLACEHOLDER_SPRINT_NAME": "Nazwa sprintu", + "PLACEHOLDER_SPRINT_START": "Oszacowany Początek", + "PLACEHOLDER_SPRINT_END": "Oszacowane Zakończenie", + "ACTION_DELETE_SPRINT": "Chcesz usunąć sprint?", + "TITLE_ACTION_DELETE_SPRINT": "usuń sprint", + "LAST_SPRINT_NAME": "ostatni sprint to: {{lastSprint}} ;-) " + }, + "CREATE_EDIT_TASK": { + "TITLE": "Nowe zadanie", + "PLACEHOLDER_SUBJECT": "Temat zadania", + "PLACEHOLDER_STATUS": "Status zadania", + "OPTION_UNASSIGNED": "Nieprzypisane", + "PLACEHOLDER_SHORT_DESCRIPTION": "Wpisz krótki opis", + "ACTION_EDIT": "Zmień zadanie" + }, + "CREATE_EDIT_US": { + "TITLE": "Nowa historyjka użytkownika", + "PLACEHOLDER_DESCRIPTION": "Dodaj proszę treściwy opis, który pomoże innym zrozumieć tę historyjkę", + "NEW_US": "Nowa historyjka użytkownika", + "EDIT_US": "Zmień historyjkę użytkownika" + }, + "DELETE_SPRINT": { + "TITLE": "Usuń sprint" + }, + "CREATE_MEMBER": { + "PLACEHOLDER_INVITATION_TEXT": "(Opcjonalne) Dodaj spersonalizowany tekst do zaproszenia. Napisz coś słodziachnego do nowego członka zespołu :)", + "PLACEHOLDER_TYPE_EMAIL": "Wpisz Email" + } + }, + "US": { + "PAGE_TITLE": "{{userStorySubject}} - Historyjka użytkownika {{userStoryRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Zakończono {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} z {{userStoryTotalTasks}} zadań). Punktów: {{userStoryPoints}}. Opis: {{userStoryDescription}}", + "SECTION_NAME": "Szczegóły historyjki użytkownika", + "LINK_TASKBOARD": "Tablica zadań", + "TITLE_LINK_TASKBOARD": "Idź do listy zadań", + "TOTAL_POINTS": "total points", + "ADD": "+ Dodaj nową historyjkę użytkownika", + "ADD_BULK": "Masowo dodaj nowe historyjki użytkownika", + "PROMOTED": "Ta historyjka awansowała ze zgłoszenia:", + "TITLE_LINK_GO_TO_ISSUE": "Idź do zgłoszenia", + "EXTERNAL_REFERENCE": "Ta historyjka została utworzona z", + "GO_TO_EXTERNAL_REFERENCE": "Idź do źródła", + "BLOCKED": "Ta historia użytkownika jest zablokowana", + "PREVIOUS": "poprzednia historia użytkownika", + "NEXT": "następna historia użytkownika", + "TITLE_DELETE_ACTION": "Usuń historyjkę użytkownika", + "LIGHTBOX_TITLE_BLOKING_US": "Blokuje nas", + "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} zadanie zakończone", + "ASSIGN": "Przypisz historyjkę użytkownika", + "NOT_ESTIMATED": "Nie oszacowane", + "TOTAL_US_POINTS": "Łącznie punktów", + "FIELDS": { + "TEAM_REQUIREMENT": "Wymaganie zespołu", + "CLIENT_REQUIREMENT": "Wymaganie klienta", + "FINISH_DATE": "Data zakończenia" + } + }, + "COMMENTS": { + "DELETED_INFO": "Komentarz usunięty przez {{user}} w dniu {{date}}", + "TITLE": "Komentarze", + "COMMENT": "Komentarz", + "TYPE_NEW_COMMENT": "Tutaj wpisz nowy komentarz", + "SHOW_DELETED": "Pokaż usunięty komentarz", + "HIDE_DELETED": "Ukryj skasowane komentarze", + "RESTORE": "Przywróć komentarz" + }, + "ACTIVITY": { + "SHOW_ACTIVITY": "Pokaż aktywność", + "DATETIME": "DD MMM YYYY HH:mm", + "SHOW_MORE": "+ Pokaż poprzednie wpisy ({{showMore}} więcej)", + "TITLE": "Aktywność", + "REMOVED": "usunięty", + "ADDED": "dodany", + "US_POINTS": "Punkty HU ({{name}})", + "NEW_ATTACHMENT": "nowy załącznik", + "DELETED_ATTACHMENT": "Usunięty załącznik", + "UPDATED_ATTACHMENT": "Zaktualizowany załącznik {{filename}}", + "DELETED_CUSTOM_ATTRIBUTE": "Usunięty niestandardowy atrybut", + "SIZE_CHANGE": "Dokonano {size, plural, one{one change} other{# changes}}", + "VALUES": { + "YES": "tak", + "NO": "nie", + "EMPTY": "pusty", + "UNASSIGNED": "nieprzypisany" + }, + "FIELDS": { + "SUBJECT": "temat", + "NAME": "imię", + "DESCRIPTION": "opis", + "CONTENT": "treść", + "STATUS": "status", + "IS_CLOSED": "zamknięty", + "FINISH_DATE": "data zakończenia", + "TYPE": "typ", + "PRIORITY": "priorytet", + "SEVERITY": "ważność", + "ASSIGNED_TO": "przypisane do", + "WATCHERS": "obserwatorzy", + "MILESTONE": "sprint", + "USER_STORY": "historyjka użytkownika", + "PROJECT": "projekt", + "IS_BLOCKED": "zablokowany", + "BLOCKED_NOTE": "notka blokady", + "POINTS": "punkty", + "CLIENT_REQUIREMENT": "wymaganie klienta", + "TEAM_REQUIREMENT": "wymaganie zespołu", + "IS_IOCAINE": "Iokaina", + "TAGS": "tagi", + "ATTACHMENTS": "załączniki", + "IS_DEPRECATED": "jest przedawniony", + "ORDER": "kolejność", + "BACKLOG_ORDER": "kolejność backlogu", + "SPRINT_ORDER": "kolejność sprintów", + "KANBAN_ORDER": "kolejność kanban", + "TASKBOARD_ORDER": "kolejność tablicy zadań", + "US_ORDER": "Kolejność HU" + } + }, + "BACKLOG": { + "PAGE_TITLE": "Backlog - {{projectName}}", + "PAGE_DESCRIPTION": "Panel backlogu zawierający historyjki użytkownika i sprinty {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Dziennik", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Admin", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", + "MOVE_US_TO_CURRENT_SPRINT": "Przejdź do bieżącego sprintu", + "SHOW_FILTERS": "Pokaż filtry", + "SHOW_TAGS": "Pokaż tagi", + "EMPTY": "Nie masz zaległości", + "CREATE_NEW_US": "Utwórz nową HU", + "CREATE_NEW_US_EMPTY_HELP": "Możesz utworzyć nową historyjkę użytkownika", + "EXCESS_OF_POINTS": "Nadwyżka punktów", + "PENDING_POINTS": "Oczekujące punkty", + "CLOSED_POINTS": "zamkniętych", + "COMPACT_SPRINT": "Kompaktuj sprint", + "GO_TO_TASKBOARD": "Idź do tablicy zadań{{::name}}", + "EDIT_SPRINT": "Edytuj sprint", + "TOTAL_POINTS": "w sumie", + "STATUS_NAME": "Nazwa statusu", + "SORTABLE_FILTER_ERROR": "Nie możesz przenosić do backlogu kiedy filtry są otwarte", + "DOOMLINE": "Zakres projektu [Doomline]", + "CHART": { + "XAXIS_LABEL": "Sprinty", + "YAXIS_LABEL": "Punkty", + "OPTIMAL": "Optymalna ilość puntów oczekujących w sprincie {{xval}} to {{yval}}", + "REAL": "Faktyczna ilość punktów oczekujących w sprincie {{xval}} to {{yval}}", + "INCREMENT_TEAM": "Punkty dodane na podstawie wymagań zespołu w sprincie {{xval}} to {{yval}}", + "INCREMENT_CLIENT": "Punkty dodane na podstawie wymagań klienta w sprincie {{xval}} to {{yval}}" + }, + "TAGS": { + "TOGGLE": "Przełącz widoczność tagów", + "SHOW": "Pokaż tagi", + "HIDE": "Ukryj tagi" + }, + "TABLE": { + "COLUMN_US": "Historyjki użytkownika", + "TITLE_COLUMN_POINTS": "Widok według ról" + }, + "SPRINT_SUMMARY": { + "TOTAL_POINTS": "łącznie
punktów", + "COMPLETED_POINTS": "wypalonych
punktów", + "OPEN_TASKS": "otwartych
zadań", + "CLOSED_TASKS": "zamkniętych
zadań", + "IOCAINE_DOSES": "dawek
Iokainy", + "SHOW_STATISTICS_TITLE": "Pokaż statystyki", + "TOGGLE_BAKLOG_GRAPH": "Pokaż/Ukryj wykres spalania" + }, + "SUMMARY": { + "PROJECT_POINTS": "punktów w
projekcie", + "DEFINED_POINTS": "zdefiniowanych
punktów", + "CLOSED_POINTS": "zamkniętych
punktów", + "POINTS_PER_SPRINT": "punktów na /
sprint" + }, + "FILTERS": { + "TOGGLE": "Przełącz widoczność filtrów", + "TITLE": "Filtry", + "REMOVE": "Usuń filtry", + "HIDE": "Ukryj filtry", + "SHOW": "Pokaż filtry", + "FILTER_CATEGORY_STATUS": "Status", + "FILTER_CATEGORY_TAGS": "Tagi" + }, + "SPRINTS": { + "TITLE": "SPRINTY", + "DATE": "DD MMM YYYY", + "LINK_TASKBOARD": "Tablica zadań sprintu", + "TITLE_LINK_TASKBOARD": "Idź do tablicy zadań użytkownika {{name}}", + "NUMBER_SPRINTS": "
sprintów", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", + "ACTION_SHOW_CLOSED_SPRINTS": "Pokaż zamknięte sprinty", + "ACTION_HIDE_CLOSED_SPRINTS": "Ukryj zamknięte sprinty" + } + }, + "ERROR": { + "TEXT1": "Wystąpił błąd, nasze Umpa Lumpy będą pracować nad jego rozwiązaniem.", + "NOT_FOUND": "Nie znaleziono", + "NOT_FOUND_TEXT": "Błąd 404 - strona której szukasz nie istnieje. Wróć do strony głównej Taiga i zobacz, czy znajdziesz tam to, czego szukasz.", + "PERMISSION_DENIED": "Dostęp zabroniony", + "PERMISSION_DENIED_TEXT": "You don't have permission to access to this page.", + "VERSION_ERROR": "Odśwież i zastosuj zmiany ponownie gdyż może ktoś przed Tobą zapisał jakieś zmiany." + }, + "TASKBOARD": { + "PAGE_TITLE": "{{sprintName}} - Lista zadań sprintu - {{projectName}}", + "PAGE_DESCRIPTION": "Sprint {{sprintName}} (od {{startDate}} do {{endDate}}) z {{projectName}}. Ukończono {{completedPercentage}}% ({{completedPoints}} z {{totalPoints}} punktów). {{openTasks}} otwartych zadań z {{totalTasks}}.", + "SECTION_NAME": "Tablica zadań", + "TITLE_ACTION_ADD": "Dodaj nowe zadanie", + "TITLE_ACTION_ADD_BULK": "Dodaj nowe zadania zbiorczo", + "TITLE_ACTION_ASSIGN": "Przydziel zadanie", + "TITLE_ACTION_EDIT": "Zmień zadanie", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", + "TABLE": { + "COLUMN": "Historyjka użytkownika", + "TITLE_ACTION_FOLD": "Zwiń kolumnę", + "TITLE_ACTION_UNFOLD": "Rozwiń kolumnę", + "TITLE_ACTION_FOLD_ROW": "Zwiń wiersz", + "TITLE_ACTION_UNFOLD_ROW": "Rozwiń wiersz", + "FIELD_POINTS": "punkty", + "ROW_UNASSIGED_TASKS_TITLE": "Nieprzypisane zadania" + }, + "CHARTS": { + "XAXIS_LABEL": "Dni", + "YAXIS_LABEL": "Punkty", + "OPTIMAL": "Optymalna ilość oczekujących punktów na dzień {{formattedDate}} to {{roundedValue}}", + "REAL": "Faktyczna ilość punktów oczekujących na dzień {{formattedDate}} to {{roundedValue}}", + "DATE": "DD MMMM YYYY" + } + }, + "TASK": { + "PAGE_TITLE": "{{taskSubject}} - Zadanie {{taskRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Opis: {{taskDescription}}", + "SECTION_NAME": "Szczegóły zadania", + "LINK_TASKBOARD": "Tablica zadań", + "TITLE_LINK_TASKBOARD": "Idź do listy zadań", + "PLACEHOLDER_SUBJECT": "Wpisz temat zadania", + "TITLE_SELECT_STATUS": "SNazwa statusu", + "OWNER_US": "To zadanie należy do", + "TITLE_LINK_GO_OWNER": "Idź do historyjki użytkownika", + "ORIGIN_US": "Źródło tego zadania to", + "TITLE_LINK_GO_ORIGIN": "Idź do historyjki użytkownika", + "BLOCKED": "To zadanie jest zablokowane", + "PREVIOUS": "poprzednie zadanie", + "NEXT": "następne zadanie", + "TITLE_DELETE_ACTION": "Usuń zadanie", + "LIGHTBOX_TITLE_BLOKING_TASK": "Blokowanie zadania", + "FIELDS": { + "MILESTONE": "Sprint", + "USER_STORY": "Historyjka użytkownika", + "IS_IOCAINE": "Iocaina" + }, + "ACTION_IOCAINE": "Iocaina", + "TITLE_ACTION_IOCAINE": "Czujesz się trochę przytłoczony zadaniem? Daj znać innym klikając na ikonę Iokainy podczas edycji zadania. Jest szansa, że staniesz się odporny na tą (fikcyjną ;) ) śmiertelną truciznę biorąc małe dawki. Z pewnością jednak da Ci ona dodatkowego kopa, który pomoże w pokonaniu nowego wyzwania i staniu się lepszym w tym co robisz!" + }, + "NOTIFICATION": { + "OK": "Wszystko ok", + "WARNING": "Ups, coś się stało...", + "WARNING_TEXT": "Twoje zmiany nie zostały zapisane z powodu błędu!", + "SAVED": "Wszystkie zmiany zapisane!", + "CLOSE": "Zamknij powiadomienie", + "MAIL": "Powiadomienia na e-mail", + "ASK_DELETE": "Czy na pewno chcesz usunąć?" + }, + "CANCEL_ACCOUNT": { + "TITLE": "Unieważnij swoje konto", + "SUBTITLE": "Przykro nam, że opuszczasz Taiga. Mamy nadzieję, że było miło :)", + "PLACEHOLDER_INPUT_TOKEN": "unieważnij token konta", + "ACTION_LEAVING": "Tak, odchodzę!", + "SUCCESS": "Nasze Umpa Lumpy usunęły twoje konto" + }, + "CHANGE_EMAIL_FORM": { + "TITLE": "Zmień swój e-mail", + "SUBTITLE": "Jeszcze tylko jeden klik i twój email będzie zaktualizowany!", + "PLACEHOLDER_INPUT_TOKEN": "zmień token e-maila", + "ACTION_CHANGE_EMAIL": "Zmień e-mail", + "SUCCESS": "Nasze Umpa Lumpy zaktualizowały Twój e-mail" + }, + "ISSUES": { + "PAGE_TITLE": "Zgłoszenia - {{projectName}}", + "PAGE_DESCRIPTION": "Lista zgłoszeń w projekcie {{projectName}}: {{projectDescription}}", + "LIST_SECTION_NAME": "Zgłoszenia", + "SECTION_NAME": "Szczegóły zgłoszenia", + "ACTION_NEW_ISSUE": "+ NOWE ZGŁOSZENIE", + "ACTION_PROMOTE_TO_US": "Awansuj na historyjkę użytkownika", + "PLACEHOLDER_FILTER_NAME": "Wpisz nazwę filtru i kliknij enter", + "PROMOTED": "To zgłoszenie zostało wypromowane na HU:", + "EXTERNAL_REFERENCE": "Źródło zgłoszenia", + "GO_TO_EXTERNAL_REFERENCE": "Idź do źródła", + "BLOCKED": "To zgłoszenie jest zablokowane", + "TITLE_PREVIOUS_ISSUE": "poprzednie zgłoszenie", + "TITLE_NEXT_ISSUE": "następne zgłoszenie", + "ACTION_DELETE": "Usuń zgłoszenie", + "LIGHTBOX_TITLE_BLOKING_ISSUE": "Blokowanie zgłoszenia", + "FIELDS": { + "PRIORITY": "Priorytet", + "SEVERITY": "Ważność", + "TYPE": "Typ" + }, + "CONFIRM_PROMOTE": { + "TITLE": "Awansuj to zgłoszenie na historyjkę użytkownika", + "MESSAGE": "Jesteś pewny, że chcesz wypromować to zgłoszenie na historyjkę użytkownika?" + }, + "FILTERS": { + "TITLE": "Filtry", + "INPUT_SEARCH_PLACEHOLDER": "Temat lub referencja", + "TITLE_ACTION_SEARCH": "Szukaj", + "ACTION_SAVE_CUSTOM_FILTER": "zapisz jako filtr niestandardowy", + "BREADCRUMB": "Filtry", + "TITLE_BREADCRUMB": "Filtry", + "CATEGORIES": { + "TYPE": "Typy", + "STATUS": "Statusy", + "SEVERITY": "Ważność", + "PRIORITIES": "Priorytety", + "TAGS": "Tagi", + "ASSIGNED_TO": "Przypisane do", + "CREATED_BY": "Stworzona przez", + "CUSTOM_FILTERS": "Filtry niestandardowe" + }, + "CONFIRM_DELETE": { + "TITLE": "Usuń filtr niestandardowy", + "MESSAGE": "filtr niestandardowy '{{customFilterName}}'" + } + }, + "TABLE": { + "COLUMNS": { + "TYPE": "Typ", + "SEVERITY": "Ważność", + "PRIORITY": "Priorytet", + "SUBJECT": "Temat", + "VOTES": "Głosy", + "STATUS": "Status", + "CREATED": "Utworzone", + "ASSIGNED_TO": "Przypisane do" + }, + "TITLE_ACTION_CHANGE_STATUS": "Zmień status", + "TITLE_ACTION_ASSIGNED_TO": "Przypisane do", + "BLOCKED": "Zablokowane", + "EMPTY": { + "TITLE": "Brak zgłoszeń :-)", + "SUBTITLE": "Masz zgłoszenie?" + } + } + }, + "ISSUE": { + "PAGE_TITLE": "{{issueSubject}} - Zgłoszenie {{issueRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Status: {{issueStatus }}. Typ: {{issueType}}, Priorytet: {{issuePriority}}. Ważność: {{issueSeverity}}. Opis: {{issueDescription}}" + }, + "KANBAN": { + "PAGE_TITLE": "Kanban - {{projectName}}", + "PAGE_DESCRIPTION": "Panel kanban zawierający historyjki użytkownika wchodzące w skład projektu {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Kanaban", + "TITLE_ACTION_FOLD": "Zwiń kolumnę", + "TITLE_ACTION_UNFOLD": "Rozwiń kolumnę", + "TITLE_ACTION_FOLD_CARDS": "Zwiń karty", + "TITLE_ACTION_UNFOLD_CARDS": "Rozwiń karty", + "TITLE_ACTION_ADD_US": "Dodaj nową HU", + "TITLE_ACTION_ADD_BULK": "Dodaj zbiorczo HU", + "ACTION_SHOW_ARCHIVED": "Pokaż archiwalne", + "ACTION_HIDE_ARCHIVED": "Ukryj archiwalne", + "HIDDEN_USER_STORIES": "Historyjki użytkownika o tym statusie są domyślnie ukryte", + "ARCHIVED": "Zarchiwizowano", + "UNDO_ARCHIVED": "Przeciągnij i upuść ponownie aby cofnąć", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" + }, + "SEARCH": { + "PAGE_TITLE": "Szukaj - {{projectName}}", + "PAGE_DESCRIPTION": "Możesz przeszukiwać wszystko, historyjki użytkownika, zgłoszenia, zadania oraz strony Wiki w projekcie {{projectName}}: {{projectDescription}}", + "FILTER_USER_STORIES": "Historyjki użytkownika", + "FILTER_ISSUES": "Zgłoszenia", + "FILTER_TASKS": "Zadania", + "FILTER_WIKI": "Strony Wiki", + "PLACEHOLDER_SEARCH": "Szukaj w ...", + "TITLE_ACTION_SEARCH": "szukaj", + "EMPTY_TITLE": "Nie znaleziono niczego, co spełniałoby podane kryteria.", + "EMPTY_DESCRIPTION": "Spróbuj użyć zakładek powyżej przy ponownym wyszukiwaniu" + }, + "TEAM": { + "PAGE_TITLE": "Zespół - {{projectName}}", + "PAGE_DESCRIPTION": "Panel zespołu w którym zebrani są wszyscy jego członkowie pracujący przy projekcie {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Zespół", + "APP_TITLE": "ZESPÓŁ - {{projectName}}", + "PLACEHOLDER_INPUT_SEARCH": "Szukaj po imieniu...", + "COLUMN_MR_WOLF": "Mr. Wolf", + "EXPLANATION_COLUMN_MR_WOLF": "Zamknięte zgłoszenia", + "COLUMN_IOCAINE": "Iokainista", + "EXPLANATION_COLUMN_IOCAINE": "Spożyte dawki", + "COLUMN_CERVANTES": "Cervantes", + "EXPLANATION_COLUMN_CERVANTES": "Edytowane strony Wiki", + "COLUMN_BUG_HUNTER": "Łowca Bugów", + "EXPLANATION_COLUMN_BUG_HUNTER": "Zgłoszone zadania", + "COLUMN_NIGHT_SHIFT": "Niezłomny", + "EXPLANATION_COLUMN_NIGHT_SHIFT": "Zamknięte zadania", + "COLUMN_TOTAL_POWER": "Ogólna potęga", + "EXPLANATION_COLUMN_TOTAL_POWER": "Punktów łącznie", + "SECTION_TITLE_TEAM": "Zespół >", + "SECTION_FILTER_ALL": "Wszystko", + "CONFIRM_LEAVE_PROJECT": "Na pewno chcesz opuścić projekt?", + "ACTION_LEAVE_PROJECT": "Opuść projekt" + }, + "USER_SETTINGS": { + "AVATAR_MAX_SIZE": "[Maks. rozmiar: {{maxFileSize}}]", + "MENU": { + "SECTION_TITLE": "Ustawienia użytkownika", + "USER_PROFILE": "Profil użytkownika", + "CHANGE_PASSWORD": "Zmień hasło", + "EMAIL_NOTIFICATIONS": "Powiadomienia e-mail" + }, + "NOTIFICATIONS": { + "SECTION_NAME": "Powiadomienia przez e-mail", + "COLUMN_PROJECT": "Projekt", + "COLUMN_RECEIVE_ALL": "Otrzymuj wszystkie", + "COLUMN_ONLY_INVOLVED": "Tylko te, w których bierzesz udział", + "COLUMN_NO_NOTIFICATIONS": "Nie otrzymuj żadnych", + "OPTION_ALL": "Wszystkie", + "OPTION_INVOLVED": "Z moim udziałem", + "OPTION_NONE": "Żadne" + }, + "POPOVER": { + "USER_PROFILE": "Profil użytkownika", + "CHANGE_PASSWORD": "Zmień hasło", + "NOTIFICATIONS": "Powiadomienia", + "FEEDBACK": "Feedback", + "TITLE_AVATAR": "Preferencje użytkownika" + } + }, + "USER_PROFILE": { + "IMAGE_HELP": "Obraz zostanie przeskalowany do wielkości 80x80px.
", + "ACTION_CHANGE_IMAGE": "Zmień", + "ACTION_USE_GRAVATAR": "Użyj Gravatara", + "ACTION_DELETE_ACCOUNT": "Usuń konto Taiga", + "CHANGE_EMAIL_SUCCESS": "Sprawdź swoją skrzynkę e-mail!
Wysłaliśmy wiadomość z instrukcjami.", + "CHANGE_PHOTO": "Zmień zdjęcie", + "FIELD": { + "USERNAME": "Nazwa użytkownika", + "EMAIL": "Email", + "FULL_NAME": "Imię i nazwisko", + "PLACEHOLDER_FULL_NAME": "Podaj swoje imię i nazwisko", + "BIO": "Bio (max. 210 znaków)", + "PLACEHOLDER_BIO": "Powiedz coś o sobie", + "LANGUAGE": "Język", + "LANGUAGE_DEFAULT": "-- używaj domyślnego języka --", + "THEME": "Szablon strony", + "THEME_DEFAULT": "-- używaj domyślnego szablonu --" + } + }, + "WIZARD": { + "SECTION_TITLE_CHOOSE_TEMPLATE": "Wybierz szablon", + "CHOOSE_TEMPLATE_TEXT": "Który szablon lepiej pasuje do Twojego projektu?", + "SECTION_TITLE_CREATE_PROJECT": "Utwórz projekt", + "CREATE_PROJECT_TEXT": "Nowy, zwinny! To takie ekscytujące!", + "PROGRESS_TEMPLATE_SELECTION": "Wybór szablonu", + "PROGRESS_NAME_DESCRIPTION": "Nazwa i opis" + }, + "WIKI": { + "PAGE_TITLE": "{{wikiPageName}} - Wiki - {{projectName}}", + "PAGE_DESCRIPTION": "Ostatnio edytowane dnia {{lastModifiedDate}} ({{totalEditions}} edycji łącznie) Zawartość: {{ wikiPageContent }}", + "DATETIME": "DD MMM YYYY HH:mm", + "PLACEHOLDER_PAGE": "Napisz swoje Wiki", + "REMOVE": "Usuń tą stronę Wiki", + "DELETE_LIGHTBOX_TITLE": "Usuń tą stronę Wiki", + "NAVIGATION": { + "SECTION_NAME": "Linki", + "ACTION_ADD_LINK": "Dodaj link" + }, + "SUMMARY": { + "TIMES_EDITED": "razy
edytowano", + "LAST_EDIT": "ostatnia
edycja", + "LAST_MODIFICATION": "ostatnia modyfikacja" + } + }, + "HINTS": { + "SECTION_NAME": "Wskazówka", + "LINK": "Jeśli chcesz się dowiedzieć jak tego używać odwiedź nasz support", + "LINK_TITLE": "Odwiedź nasz support", + "HINT1_TITLE": "Czy wiesz, że możesz eksportować i importować projekty?", + "HINT1_TEXT": "Dzięki temu możesz przenieść wszystkie swoje dane z jednej instalacji Taiga do drugiej.", + "HINT2_TITLE": "Czy wiesz, że możesz tworzyć niestandardowe pola?", + "HINT2_TEXT": "Zespoły mogą tworzyć niestandardowe pola co pozwala dopasować formularze do sposobu pracy.", + "HINT3_TITLE": "Zmieniaj kolejność projektów aby wyróżnić te najbardziej istotne dla Ciebie", + "HINT3_TEXT": "Pierwsze 10 projektów będzie wyświetlane w górnym menu", + "HINT4_TITLE": "Zapomniałeś nad czym pracujesz?", + "HINT4_TEXT": "Nie martw się, na Twojej tablicy znajdziesz otwarte zadania, zgłoszenia i historyjki użytkownika w takiej kolejności, w jakiej nad nimi pracowałeś." + }, + "TIMELINE": { + "UPLOAD_ATTACHMENT": "Użytkownik {{username}} wysłał nowy załącznik do {{obj_name}}", + "US_CREATED": "Użytkownik {{username}} utworzył nową historyjkę użytkownika {{obj_name}} w projekcie {{project_name}}", + "ISSUE_CREATED": "Użytkownik {{username}} utworzył nowe zgłoszenie {{obj_name}} w projekcie {{project_name}}", + "TASK_CREATED": "Użytkownik {{username}} utworzył nowe zadanie {{obj_name}} w projekcie {{project_name}}", + "TASK_CREATED_WITH_US": "Użytkownik {{username}} utworzył nowe zadanie {{obj_name}} w projekcie {{project_name}} należące do HU {{us_name}}", + "WIKI_CREATED": "Użytkownik {{username}} utworzył nową stronę Wiki {{obj_name}} w projekcie {{project_name}}", + "MILESTONE_CREATED": "Użytkownik {{username}} utworzył nowy sprint {{obj_name}} w projekcie {{project_name}}", + "NEW_PROJECT": "Użytkownik {{username}} utworzył projekt {{project_name}}", + "MILESTONE_UPDATED": "Użytkownik {{username}} zaktualizował sprint {{obj_name}}", + "US_UPDATED": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} historyjki użytkownika {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} historyjki użytkownika {{obj_name}} na {{new_value}}", + "US_UPDATED_POINTS": "Użytkownik {{username}} zaktualizował '{{role_name}}' punkty w historyjce użytkownika {{obj_name}} na {{new_value}}", + "ISSUE_UPDATED": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zgłoszenia {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zgłoszenia {{obj_name}} na {{new_value}}", + "TASK_UPDATED": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zadania {{obj_name}} na {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zadania {{obj_name}} na {{new_value}}", + "TASK_UPDATED_WITH_US": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zadania {{obj_name}} należącego do HU {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "Użytkownik {{username}} zaktualizował atrybut {{field_name}} zadania {{obj_name}} należącego do HU {{us_name}} na {{new_value}}", + "WIKI_UPDATED": "Użytkownik {{username}} zaktualizował stronę Wiki {{obj_name}}", + "NEW_COMMENT_US": "Użytkownik {{username}} skomentował historyjkę użytkownika {{obj_name}}", + "NEW_COMMENT_ISSUE": "Użytkownik {{username}} skomentował zgłoszenie {{obj_name}}", + "NEW_COMMENT_TASK": "Użytkownik {{username}} skomentował zadanie {{obj_name}}", + "NEW_MEMBER": "Projekt {{project_name}} ma nowego członka", + "US_ADDED_MILESTONE": "Użytkownik{{username}} dodał HU {{obj_name}} do {{sprint_name}}", + "US_MOVED": "{{username}} przeniósł historyjkę użytkownika {{obj_name}}", + "US_REMOVED_FROM_MILESTONE": "Użytkownik {{username}} dodał HU {{obj_name}} do backlogu", + "BLOCKED": "Użytkownik {{username}} zablokował {{obj_name}}", + "UNBLOCKED": "Użytkownik {{username}} odblokował {{obj_name}}", + "NEW_USER": "Nowy użytkownik{{username}} dołączył do Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "An external app requires authentication", + "PAGE_DESCRIPTION": "An external app requires authentication", + "AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?", + "LOGIN_WITH_ANOTHER_USER": "Login with another user", + "AUTHORIZE_APP": "Authorize app", + "CANCEL": "Anuluj" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Twój projekt", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Pracujesz nad", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Obserwujesz", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprinty", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "Historyjki użytkownika", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } + } +} \ No newline at end of file diff --git a/app/locales/locale-pt-br.json b/app/locales/locale-pt-br.json new file mode 100644 index 00000000..cbc6eda8 --- /dev/null +++ b/app/locales/locale-pt-br.json @@ -0,0 +1,1411 @@ +{ + "COMMON": { + "YES": "Sim", + "NO": "Não", + "LOADING": "Carregando...", + "LOADING_PROJECT": "Carregando o projeto...", + "DATE": "DD MMM AAAA", + "DATETIME": "DD MMM AAAA HH:mm", + "SAVE": "Salvar", + "CANCEL": "Cancelar", + "ACCEPT": "Aceitar", + "DELETE": "Apagar", + "CREATE": "Criar", + "ADD": "Adicionar", + "COPY_TO_CLIPBOARD": "Copiar para a área de transferência: Ctrl+C", + "EDIT": "Editar", + "DRAG": "Arrastar", + "TAG_LINE": "Sua ferramenta de código aberto, gratuita e ágil.", + "TAG_LINE_2": "AME SEU PROJETO", + "BLOCK": "Bloquear", + "UNBLOCK": "Desbloquear", + "BLOCKED": "Bloqueado", + "CREATED_BY": "Criado por {{fullDisplayName}}", + "FROM": "de", + "TO": "para", + "CLOSE": "fechar", + "BLOCKED_NOTE": "Por que esta User Story foi bloqueada? ", + "BLOCKED_REASON": "Por favor, explique a razão", + "GO_HOME": "Ir ao início", + "PLUGINS": "Plugins", + "BETA": "Estamos em beta!", + "ONE_ITEM_LINE": "Um item por linha...", + "NEW_BULK": "Nova inserção em lote", + "RELATED_TASKS": "Tarefas relacionadas", + "LOGOUT": "Sair", + "EXTERNAL_USER": "um usuário externo", + "GENERIC_ERROR": "Um Oompa Loompas disse {{error}}.", + "IOCAINE_TEXT": "Se sentindo sobrecarregado por uma tarefa? Assegure-se de que os outros saibam disso clicando em Iocaine quando estiver editando a tarefa. É possível se tornar imune a essse veneno mortal (fictício) consumindo pequenas quantidades ao longo do tempo, assim como é possível ficar melhor no que faz, ocasionalmente, por assumir desafios extras!", + "CAPSLOCK_WARNING": "Seja cuidadoso! Você está escrevendo em letras maiúsculas e esse campo é case sensitive, ou seja trata com distinção letras maiúsculas das minúsculas", + "FORM_ERRORS": { + "DEFAULT_MESSAGE": "Este valor parece ser inválido.", + "TYPE_EMAIL": "Este valor deve ser um e-mail válido.", + "TYPE_URL": "Este valor deve ser uma url válida.", + "TYPE_URLSTRICT": "Este valor deve ser uma url válida.", + "TYPE_NUMBER": "Este valor deve ser um número válido.", + "TYPE_DIGITS": "Este valor deve ser dígitos.", + "TYPE_DATEISO": "Este valor deve ser uma data válida (YYYY-MM-DD).", + "TYPE_ALPHANUM": "Este valor deve ser alfanumérico.", + "TYPE_PHONE": "Este valor deveria ser um número telefônico valido.", + "NOTNULL": "Este valor não deveria ser nulo.", + "NOT_BLANK": "Esta campo não deveria esta em branco.", + "REQUIRED": "Este valor é obrigatório.", + "REGEXP": "Este valor parece ser inválido.", + "MIN": "O valor deve ser maior que ou igual a %s.", + "MAX": "Esse valor deve ser menor que ou igual a %s.", + "RANGE": "Esse valor deve ser entre %s e %s.", + "MIN_LENGTH": "Esse valor é muito curto. deve ter %s caracteres ou mais.", + "MAX_LENGTH": "Valor muito longo. O campo deve conter %s caracteres ou menos.", + "RANGE_LENGTH": "Esse comprimento de valor é inválido. Deve ser entre %s e %s caracteres de comprimento.", + "MIN_CHECK": "Você deve selecionar pelo menos %s escolhas.", + "MAX_CHECK": "Você deve selecionar %s escolhas ou menos.", + "RANGE_CHECK": "Você deve selecionar entre %s e %s escolhas.", + "EQUAL_TO": "Esse valor deveria ser o mesmo." + }, + "PICKERDATE": { + "FORMAT": "DD MMM YYYY", + "IS_RTL": "falso", + "FIRST_DAY_OF_WEEK": "1", + "PREV_MONTH": "Mês Anterior", + "NEXT_MONTH": "Próximo Mês", + "MONTHS": { + "JAN": "Janeiro", + "FEB": "Fevereiro", + "MAR": "Março", + "APR": "Abril", + "MAY": "Maio", + "JUN": "Junho", + "JUL": "Julho", + "AUG": "Agosto", + "SEP": "Setembro", + "OCT": "Outubro", + "NOV": "Novembro", + "DEC": "Dezembro" + }, + "WEEK_DAYS": { + "SUN": "Domingo", + "MON": "Segunda-feira", + "TUE": "Terça-feira", + "WED": "Quarta-feira", + "THU": "Quinta-feira", + "FRI": "Sexta-feira", + "SAT": "Sábado" + }, + "WEEK_DAYS_SHORT": { + "SUN": "Dom", + "MON": "Seg", + "TUE": "Ter", + "WED": "Qua", + "THU": "Qui", + "FRI": "Sex", + "SAT": "Sab" + } + }, + "SEE_USER_PROFILE": "Ver o perfil de {{username }}", + "USER_STORY": "User Story", + "TASK": "Tarefa", + "ISSUE": "caso", + "TAGS": { + "PLACEHOLDER": "To dentro! Me tagueie....", + "DELETE": "Apagar tag", + "ADD": "Adicionar tag" + }, + "DESCRIPTION": { + "EMPTY": "Espaço vazio é tãããooo monótono... escreva algo, vai...", + "NO_DESCRIPTION": "Sem descrição ainda" + }, + "FIELDS": { + "SUBJECT": "Assunto", + "NAME": "Nome", + "URL": "URL", + "DESCRIPTION": "Descrição", + "VALUE": "Valor", + "SLUG": "Rótulo", + "COLOR": "Cor", + "IS_CLOSED": "Está fechado?", + "STATUS": "Situação", + "TYPE": "Tipo", + "SEVERITY": "Severidade", + "PRIORITY": "Prioridade", + "ASSIGNED_TO": "Atribuído a", + "POINTS": "Pontos", + "BLOCKED_NOTE": "Nota de bloqueio", + "IS_BLOCKED": "está bloqueada", + "REF": "Ref", + "VOTES": "Votos" + }, + "ROLES": { + "ALL": "Tudo" + }, + "ASSIGNED_TO": { + "NOT_ASSIGNED": "Não assinado", + "DELETE_ASSIGNMENT": "Apagar atribuição", + "REMOVE_ASSIGNED": "Remover assinatura", + "TOO_MANY": "...muitos usuários, continue filtrando", + "CONFIRM_UNASSIGNED": "Você tem certeza que deseja deixar sem ", + "TITLE_ACTION_EDIT_ASSIGNMENT": "E" + }, + "STATUS": { + "CLOSED": "Fechado", + "OPEN": "Aberto" + }, + "WATCHERS": { + "ADD": "Adicionar observadores", + "TITLE_ADD": "Adicionar um membro do projeto para a lista de observadores", + "DELETE": "Apagar observador", + "TITLE_LIGHTBOX_DELETE_WARTCHER": "Apagar observador..." + }, + "WATCH_BUTTON": { + "WATCH": "Observar", + "WATCHING": "Observando", + "UNWATCH": "Deixar de Observar", + "WATCHERS": "Observadores", + "BUTTON_TITLE": "Observar/Deixar de observar este item", + "COUNTER_TITLE": "{total, plural, um{um observador} outro{#watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Aprovar", + "UPVOTED": "Aprovado", + "DOWNVOTE": "Reprovar", + "VOTERS": "Eleitores", + "BUTTON_TITLE": "Aprovar/Reprovar este item", + "COUNTER_TITLE": "{total, plural, um{um voto} outro{# votos}}" + }, + "CUSTOM_ATTRIBUTES": { + "CUSTOM_FIELDS": "Campos Personalizados", + "SAVE": "Salvar Campo Personalizado", + "EDIT": "Editar Campo Personalizado", + "DELETE": "Apagar atributo personalizado", + "CONFIRM_DELETE": "Lembre-se que todos os valores deste campo personalizado serão apagados.
Tem certeza que quer continuar?" + }, + "FILTERS": { + "TITLE": "filtros", + "INPUT_PLACEHOLDER": "Assunto ou referência", + "TITLE_ACTION_FILTER_BUTTON": "procurar", + "BREADCRUMB_TITLE": "voltar para categorias", + "BREADCRUMB_FILTERS": "Filtros", + "BREADCRUMB_STATUS": "Situação" + }, + "WYSIWYG": { + "H1_BUTTON": "Primeira caixa de cabeçalho", + "H1_SAMPLE_TEXT": "Seu título aqui...", + "H2_BUTTON": "Segundo caixa de cabeçalho", + "H2_SAMPLE_TEXT": "Seu título aqui...", + "H3_BUTTON": "Terceira caixa de cabeçalho", + "H3_SAMPLE_TEXT": "Seu título aqui...", + "BOLD_BUTTON": "Negrito", + "BOLD_BUTTON_SAMPLE_TEXT": "Seu texto aqui...", + "ITALIC_BUTTON": "Itálico", + "ITALIC_SAMPLE_TEXT": "Seu texto aqui...", + "STRIKE_BUTTON": "Strike", + "STRIKE_SAMPLE_TEXT": "Seu texto aqui...", + "BULLETED_LIST_BUTTON": "Lista de Marcadores", + "BULLETED_LIST_SAMPLE_TEXT": "Seu texto aqui...", + "NUMERIC_LIST_BUTTON": "Lista Numérica", + "NUMERIC_LIST_SAMPLE_TEXT": "Seu texto aqui...", + "PICTURE_BUTTON": "Imagem", + "PICTURE_SAMPLE_TEXT": "Seu texto alternativo para a imagem aqui...", + "LINK_BUTTON": "Link", + "LINK_SAMPLE_TEXT": "Seu texto para o link vai aqui...", + "QUOTE_BLOCK_BUTTON": "Bloco de citação", + "QUOTE_BLOCK_SAMPLE_TEXT": "Seu texto aqui...", + "CODE_BLOCK_BUTTON": "Bloco de Código", + "CODE_BLOCK_SAMPLE_TEXT": "Seu texto aqui...", + "PREVIEW_BUTTON": "Pré Visualizar", + "EDIT_BUTTON": "Editar", + "MARKDOWN_HELP": "Ajuda de sintaxe markdown" + }, + "PERMISIONS_CATEGORIES": { + "SPRINTS": { + "NAME": "Sprints", + "VIEW_SPRINTS": "Ver sprints", + "ADD_SPRINTS": "Adicionar sprints", + "MODIFY_SPRINTS": "Modificar sprints", + "DELETE_SPRINTS": "Apagar Sprints" + }, + "USER_STORIES": { + "NAME": "User Stories", + "VIEW_USER_STORIES": "Ver user stories", + "ADD_USER_STORIES": "Adicionar user stories", + "MODIFY_USER_STORIES": "Modificar user stories", + "DELETE_USER_STORIES": "Apagar user stories" + }, + "TASKS": { + "NAME": "Tarefas", + "VIEW_TASKS": "Ver tarefas", + "ADD_TASKS": "Adicionar uma nova Tarefa", + "MODIFY_TASKS": "Modificar tarefa", + "DELETE_TASKS": "Apagar tarefas" + }, + "ISSUES": { + "NAME": "Casos", + "VIEW_ISSUES": "Ver casos", + "ADD_ISSUES": "Adicionar casos", + "MODIFY_ISSUES": "Modificar casos", + "DELETE_ISSUES": "Apagar casos" + }, + "WIKI": { + "NAME": "Wiki", + "VIEW_WIKI_PAGES": "Exibir páginas da wiki", + "ADD_WIKI_PAGES": "Adicionar páginas wiki", + "MODIFY_WIKI_PAGES": "Modificar páginas wiki", + "DELETE_WIKI_PAGES": "Apagar páginas wiki", + "VIEW_WIKI_LINKS": "Exibir links da wiki", + "ADD_WIKI_LINKS": "Adicionar links da wiki", + "DELETE_WIKI_LINKS": "Apagar links da wiki" + } + }, + "META": { + "PAGE_TITLE": "Taiga", + "PAGE_DESCRIPTION": "Taiga é uma plataforma de gerenciamento de projetos para startups e desenvolvedores ágeis & designers que querem uma simples, bela ferramenta que faça o trabalho ser agradável." + } + }, + "LOGIN": { + "PAGE_TITLE": "Login - Taiga", + "PAGE_DESCRIPTION": "Entrando no Taiga, uma plataforma de gerenciamento de projetos para startups com desenvolvedores e designers ágeis que desejam uma ferramenta simples e bela que torne o trabalho realmente agradável." + }, + "AUTH": { + "INVITED_YOU": "convidou você para entrar no projeto", + "NOT_REGISTERED_YET": "Não é cadastrado ainda?", + "REGISTER": "Registrar", + "CREATE_ACCOUNT": "crie sua conta grátis aqui" + }, + "LOGIN_COMMON": { + "HEADER": "Eu já tenho um login Taiga", + "PLACEHOLDER_AUTH_NAME": "Nome de usuário ou email (case sensitive)", + "LINK_FORGOT_PASSWORD": "Esqueceu?", + "TITLE_LINK_FORGOT_PASSWORD": "Você esqueceu sua senha?", + "ACTION_ENTER": "Entre", + "ACTION_SIGN_IN": "Identifique-se", + "PLACEHOLDER_AUTH_PASSWORD": "Senha (case sensitive)" + }, + "LOGIN_FORM": { + "ERROR_AUTH_INCORRECT": "De acordo com nossos Oompa Loompas, seu nome de usuario/email ou senha estão incorretos.", + "SUCCESS": "Nossos Oompa Loompas estão felizes, bem vindo ao Taiga." + }, + "REGISTER": { + "PAGE_TITLE": "Cadastre-se - Taiga", + "PAGE_DESCRIPTION": "Crie sua conta no Taiga, uma plataforma de gerenciamento de projetos para startups e desenvolvedores ágeis & designers que desejam uma ferramenta bela e simples que torne o trabalho realmente agradável.\"" + }, + "REGISTER_FORM": { + "TITLE": "Registre uma nova conta Taiga (Grátis)", + "PLACEHOLDER_NAME": "Escolha um nome de usuário (case sensitive)", + "PLACEHOLDER_FULL_NAME": "Insira seu nome completo", + "PLACEHOLDER_EMAIL": "Seu email", + "PLACEHOLDER_PASSWORD": "Escolha uma senha (case sensitive)", + "ACTION_SIGN_UP": "Registre-se", + "TITLE_LINK_LOGIN": "Identifique-se", + "LINK_LOGIN": "Você já está registrado? Idenfique-se" + }, + "FORGOT_PASSWORD": { + "PAGE_TITLE": "Esqueceu sua senha - Taiga", + "PAGE_DESCRIPTION": "Informe seu nome de usuário ou email para receber uma nova senha e ter acesso ao Taiga novamente." + }, + "FORGOT_PASSWORD_FORM": { + "TITLE": "Opaaa, você esqueceu sua senha?", + "SUBTITLE": "Entre seu usuário ou email para conseguir uma senha nova", + "PLACEHOLDER_FIELD": "Nome de usuário ou email", + "ACTION_RESET_PASSWORD": "Resetar Senha", + "LINK_CANCEL": "Nããão, me leve de volta. Acho que me lembrei.", + "SUCCESS": "Verifique sua conta de email
Enviamos um email com as instruções para configurar uma nova senha", + "ERROR": "Segundo nossos Oompa Loompas, você ainda não está inscrito." + }, + "CHANGE_PASSWORD": { + "PAGE_TITLE": "Alterar sua senha - Taiga", + "PAGE_DESCRIPTION": "Escolher nova senha para o seu Taiga e hey!, você deveria comer comida rica em ferro, é ótimo para o cérebro :P", + "SECTION_NAME": "Alterar senha", + "FIELD_CURRENT_PASSWORD": "Senha atual", + "PLACEHOLDER_CURRENT_PASSWORD": "Sua senha atual (ou vazio caso ainda não tenha uma senha)", + "FIELD_NEW_PASSWORD": "Nova senha", + "PLACEHOLDER_NEW_PASSWORD": "Digite a nova senha", + "FIELD_RETYPE_PASSWORD": "Redigite a nova senha", + "PLACEHOLDER_RETYPE_PASSWORD": "Digite novamente a sua nova senha", + "ERROR_PASSWORD_MATCH": "As senhas não coincidem" + }, + "CHANGE_PASSWORD_RECOVERY_FORM": { + "TITLE": "Criar um novo passe Taiga", + "SUBTITLE": "Hey, você deveria comer comida rica em ferro, é ótimo para o cérebro :P", + "PLACEHOLDER_NEW_PASSWORD": "Nova senha", + "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Redigite a nova senha", + "ACTION_RESET_PASSWORD": "Resetar Senha", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", + "SUCCESS": "Nossos Oompa Loompas salvaram sua nova senha.
Tente identificar-se com ela." + }, + "INVITATION": { + "PAGE_TITLE": "Aceitando um convite - Taiga", + "PAGE_DESCRIPTION": "Aceitar o convite para participar de um projeto no Taiga, uma plataforma de gerenciamento de projetos para startups e desenvolvedores ágeis & designers que desejam uma ferramenta bela e simples que torne o trabalho realmente agradável." + }, + "INVITATION_LOGIN_FORM": { + "NOT_FOUND": "Nossos Oompa Loompas não puderam encontrar seu convite.", + "SUCCESS": "Você ingressou com sucesso nesse projeto, Seja bem vindo ao projeto {{project_name}}", + "ERROR": "De acordo com nossos Oompa Loompas, você não está inscrito ou digitou uma senha inválida." + }, + "HOME": { + "PAGE_TITLE": "Início - Taiga", + "PAGE_DESCRIPTION": "A página inicial do Taiga contém seus projetos principais e todos as user stories atribuídas ou observadas por você, tarefas e casos", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", + "EMPTY_PROJECT_LIST": "Você ainda não tem projetos", + "WORKING_ON_SECTION": "Trabalhando em", + "WATCHING_SECTION": "Observando" + }, + "PROJECTS": { + "PAGE_TITLE": "Meus projetos - Taiga", + "PAGE_DESCRIPTION": "Uma lista com todos os seus projetos, você pode reorganizá-los ou criar um novo.", + "MY_PROJECTS": "Meus Projetos" + }, + "ATTACHMENT": { + "SECTION_NAME": "anexos", + "TITLE": "{{ fileName }} enviado em {{ date }}", + "DESCRIPTION": "Escreva uma curta descrição", + "DEPRECATED": "(obsoleto)", + "DEPRECATED_FILE": "Obsoleto?", + "ADD": "Adicionar novo anexo. {{maxFileSizeMsg}}", + "MAX_FILE_SIZE": "[Tamanho Máximo: {{maxFileSize}}]", + "SHOW_DEPRECATED": "+ mostrar anexos deprecados", + "HIDE_DEPRECATED": "- esconder anexos obsoletos", + "COUNT_DEPRECATED": "({{ counter }} deprecados)", + "MAX_UPLOAD_SIZE": "Tamanho máximo de upload é {{maxFileSize}}", + "DATE": "DD MMM YYYY [at] hh:mm", + "ERROR_UPLOAD_ATTACHMENT": "Não fomos capazes de carregar '{{fileName}}'.\n{{errorMessage}}", + "TITLE_LIGHTBOX_DELETE_ATTACHMENT": "Apagar anexo...", + "MSG_LIGHTBOX_DELETE_ATTACHMENT": "o anexo '{{fileName}}'", + "ERROR_DELETE_ATTACHMENT": "Não fomos capazes de apagar: {{errorMessage}}", + "FIELDS": { + "IS_DEPRECATED": "está obsoleto" + } + }, + "PAGINATION": { + "PREVIOUS": "Ant", + "NEXT": "Próximo" + }, + "ADMIN": { + "COMMON": { + "TITLE_ACTION_EDIT_VALUE": "Editar valor", + "TITLE_ACTION_DELETE_VALUE": "Apagar valor" + }, + "HELP": "Você precisa de ajuda? Verifique nossa pagina de suporte!", + "PROJECT_DEFAULT_VALUES": { + "TITLE": "Valores Padrão", + "SUBTITLE": "Seleciona valores default para todos os seletores" + }, + "MEMBERSHIPS": { + "TITLE": "Gerenciar Membros", + "PAGE_TITLE": "Filiados - {{projectName}}", + "ADD_BUTTON": "+ Novo Membro", + "ADD_BUTTON_TITLE": "Adicionar novo membro" + }, + "PROJECT_EXPORT": { + "TITLE": "Exportar", + "SUBTITLE": "Exporte seu projeto para guardar um backup ou para criar um novo a partir deste.", + "EXPORT_BUTTON": "Exportar", + "EXPORT_BUTTON_TITLE": "Exportar seu projeto", + "LOADING_TITLE": "Estamos gernado o arquivo de dump", + "DUMP_READY": "Seu arquivo de dump está pronto!", + "LOADING_MESSAGE": "Por favor, não feche esta pagina.", + "ASYNC_MESSAGE": "Enviaremos um email para você assim que estiver pronto.", + "SYNC_MESSAGE": "Se o download não iniciar automáticamente clique aqui", + "ERROR": "Nossos Oompa Loopmas tiveram alguns problemas gerando o dump. Por favor, tente novamente.", + "ERROR_BUSY": "Desculpe, nossos Oompa Loompas estão muito ocupados neste instante. Tente novamente em alguns minutos.", + "ERROR_MESSAGE": "Nossos Oompa Loompas tiveram alguns problemas gerando o seu dump: {{message}}" + }, + "MODULES": { + "TITLE": "Modulos", + "ENABLE": "Habilitar", + "DISABLE": "Desabilitar", + "BACKLOG": "Backlog", + "BACKLOG_DESCRIPTION": "Gerencie suas user stories para manter uma visualização organizada de trabalhos futuros e priorizados.", + "KANBAN": "Kanban", + "KANBAN_DESCRIPTION": "Organize seu projeto de um jeito \"lean\" com esse mural", + "ISSUES": "Casos", + "ISSUES_DESCRIPTION": "Acompanhe os bugs, questões e melhorias relacionadas ao seu projeto. Não perca nada!", + "WIKI": "Wiki", + "WIKI_DESCRIPTION": "Adicione, modifique ou apague conteúdo em colaboração com outras pessoas. Este é o local certo pra documentação do seu projeto.", + "MEETUP": "Reunião", + "MEETUP_DESCRIPTION": "Escolha seu sistema de vídeo conferência. Até desenvolvedores precisam de contato cara a cara.", + "SELECT_VIDEOCONFERENCE": "Selecione um sistema de video conferência", + "SALT_CHAT_ROOM": "Se você quiser, pode acrescentar um 'salt code' para o nome da sala de bate-papo", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Personalizar", + "URL_CHAT_ROOM": "URL da sua sala de bate-papo" + }, + "PROJECT_PROFILE": { + "PAGE_TITLE": "{{sectionName}} - Perfil do projeto - {{projectName}}", + "PROJECT_DETAILS": "Detalhes do projeto", + "PROJECT_NAME": "Nome do projeto", + "PROJECT_SLUG": "Slug do projeto", + "NUMBER_SPRINTS": "Número de sprints (0 para uma quantidade indeterminada)", + "NUMBER_US_POINTS": "Número de pontos US (0 para uma quantidade indeterminada)", + "TAGS": "Tags", + "DESCRIPTION": "Descrição", + "PUBLIC_PROJECT": "Projeto Publico", + "PRIVATE_PROJECT": "Projeto Privado", + "DELETE": "Apagar este projeto" + }, + "REPORTS": { + "TITLE": "Relatórios", + "SUBTITLE": "Exporte os dados do seu projeto no formato CSV e faça seus próprios relatórios.", + "DESCRIPTION": "Baixe o arquivo CSV ou copie a URL gerada e abra no seu editor de texto favorito ou planilha para fazer seu próprio relatório. Você poderá visualizar e analisar todos os seus dados facilmente.", + "HELP": "Como utilizar isto em minha própria planilha?", + "REGENERATE_TITLE": "Mudar URL", + "REGENERATE_SUBTITLE": "Você está prestes a alterar a url de acesso a dados do CSV. A URL anterior será desabilitada. Você está certo disso?" + }, + "CSV": { + "SECTION_TITLE_US": "Relatórios de user stories", + "SECTION_TITLE_TASK": "relatórios de tarefas", + "SECTION_TITLE_ISSUE": "relatórios de casos", + "DOWNLOAD": "Baixar CSV", + "URL_FIELD_PLACEHOLDER": "Por favor, gere novamente a url do CSV", + "TITLE_REGENERATE_URL": "Regerar URL CSV", + "ACTION_GENERATE_URL": "Gerar URL", + "ACTION_REGENERATE": "Regenerar" + }, + "CUSTOM_FIELDS": { + "TITLE": "Campos Personalizados", + "SUBTITLE": "Especificar campos personalizados para user stories, tarefas e casos", + "US_DESCRIPTION": "Campos personalizados das user stories", + "US_ADD": "Adicionar campo personalizado nas user stories", + "TASK_DESCRIPTION": "Campos personalizados das Tarefas\n", + "TASK_ADD": "Adicionar campos personalizados na tarefa", + "ISSUE_DESCRIPTION": "Campos personalizados dos casos", + "ISSUE_ADD": "Adicionar um campo personalizado no caso", + "FIELD_TYPE_TEXT": "Texto", + "FIELD_TYPE_MULTI": "Multi-linha", + "FIELD_TYPE_DATE": "Data" + }, + "PROJECT_VALUES": { + "PAGE_TITLE": "{{sectionName}} - Valores do projeto - {{projectName}}", + "REPLACEMENT": "Todos os itens com este valor serão modificados para", + "ERROR_DELETE_ALL": "Você não pode apagar todos os valores." + }, + "PROJECT_VALUES_POINTS": { + "TITLE": "Pontos", + "SUBTITLE": "Especifique os pontos de suas user stories poderiam ser estimados ", + "US_TITLE": "Pontos de User Stories", + "ACTION_ADD": "Adicionar novo ponto" + }, + "PROJECT_VALUES_PRIORITIES": { + "TITLE": "Prioridades", + "SUBTITLE": "Especifique as prioridades que seus casos terão", + "ISSUE_TITLE": "Prioridades do caso", + "ACTION_ADD": "Adicionar nova prioridade" + }, + "PROJECT_VALUES_SEVERITIES": { + "TITLE": "Seriedades", + "SUBTITLE": "Especifique a severidade que seus casos terão", + "ISSUE_TITLE": "Seriedades do caso", + "ACTION_ADD": "Adicionar nova severidade" + }, + "PROJECT_VALUES_STATUS": { + "TITLE": "Situação", + "SUBTITLE": "Especifique os status pelos quais suas user stories, tarefas e casos passarão", + "US_TITLE": "Estados das Users Strories", + "TASK_TITLE": "Estados da tarefa", + "ISSUE_TITLE": "Estados do caso" + }, + "PROJECT_VALUES_TYPES": { + "TITLE": "Tipos", + "SUBTITLE": "Especifique os tipos que seu caso pode ser", + "ISSUE_TITLE": "Tipos de casos", + "ACTION_ADD": "Adicionar novo {{objName}}" + }, + "ROLES": { + "PAGE_TITLE": "Funções - {{projectName}}", + "WARNING_NO_ROLE": "Seja cuidadoso, nenhuma função em seu projeto será capaz de estimar o valor dos pontos para as user stories", + "HELP_ROLE_ENABLED": "Quando habilitado, membros atribuídos a esta função serão capazes de estimar valores para user stories", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Desativar estimativa para esta permissão", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "Se você desativar a permissão de estimativas para esta regra {{roleName}} todas estimativas feitas anteriormente por esta regra serão removidas.", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Você tem certeza que quer desativar esta permissão de estimativas?", + "COUNT_MEMBERS": "{{ role.members_count }} membros com a mesma função", + "TITLE_DELETE_ROLE": "Apagar Função", + "REPLACEMENT_ROLE": "Todos os usuários com essa função serão movidos para", + "WARNING_DELETE_ROLE": "Seja cuidadoso, todas as estimativas para as funções serão removidas.", + "ERROR_DELETE_ALL": "Você não pode apagar todos os valores", + "EXTERNAL_USER": "Usuário externo" + }, + "THIRD_PARTIES": { + "SECRET_KEY": "Chave secreta", + "PAYLOAD_URL": "URL da Payload", + "VALID_IPS": "IPs de origem válidos (separados por vírgulas)" + }, + "BITBUCKET": { + "SECTION_NAME": "Bitbucket", + "PAGE_TITLE": "Bitbucket - {{projectName}}", + "INFO_VERIFYING_IP": "Requisições do Bitbucket não são assinadas, ou seja, a melhor maneira de verificar a origem é por IP. Se o campo estiver vazio não será realizada verificação de IP." + }, + "GITLAB": { + "SECTION_NAME": "Gitlab", + "PAGE_TITLE": "Gitlab - {{projectName}}", + "INFO_VERIFYING_IP": "Requisições do Gitlab não são assinadas, ou seja, a melhor maneira de verificar a origem é por IP. Se o campo estiver vazio não será realizada verificação de IP." + }, + "GITHUB": { + "SECTION_NAME": "Github", + "PAGE_TITLE": "Github - {{projectName}}" + }, + "WEBHOOKS": { + "PAGE_TITLE": "Webhooks - {{projectName}}", + "SECTION_NAME": "Webhooks", + "SUBTITLE": "Webhooks notificam serviços externos sobre eventos no Taiga, como comentários, user stories....", + "ADD_NEW": "Adicionar um Novo Webhook", + "TYPE_NAME": "Digite o nome do serviço", + "TYPE_PAYLOAD_URL": "Digite a url do serviço Payload", + "TYPE_SERVICE_SECRET": "Escreva a palavra segredo do serviço", + "SAVE": "Salvar Webhook", + "CANCEL": "Cancelar Webhook", + "SHOW_HISTORY": "(Mostrar história)", + "TEST": "Testar Webhook", + "EDIT": "Editar Webhook", + "DELETE": "Apagar Webhook", + "REQUEST": "Solicitação", + "RESEND_REQUEST": "Reenviar pedido", + "HEADERS": "Cabeçalhos", + "PAYLOAD": "Payload", + "RESPONSE": "Resposta", + "DATE": "DD MMM YYYY [at] hh:mm:ss", + "ACTION_HIDE_HISTORY": "(Esconder história)", + "ACTION_HIDE_HISTORY_TITLE": "Esconder os detalhes da história", + "ACTION_SHOW_HISTORY": "(Mostrar história)", + "ACTION_SHOW_HISTORY_TITLE": "Mostrar detalhes da história", + "WEBHOOK_NAME": "Webhook '{{name}}'" + }, + "CUSTOM_ATTRIBUTES": { + "PAGE_TITLE": "{{sectionName}} - Atributos personalizados - {{projectName}}", + "ADD": "Adicionar campos personalizados", + "EDIT": "Editar Campo Personalizado", + "DELETE": "Apagar campo personalizado", + "SAVE_TITLE": "Salvar Campo Personalizado", + "CANCEL_TITLE": "Cancelar criação", + "SET_FIELD_NAME": "Definir nome do seu campo personalizados", + "SET_FIELD_DESCRIPTION": "Definir a descrição do seu campo personalizado", + "ACTION_UPDATE": "Atualizar campo personalizado", + "ACTION_CANCEL_EDITION": "Cancelar edição" + }, + "MEMBERSHIP": { + "COLUMN_MEMBER": "Membro", + "COLUMN_ADMIN": "Administrador", + "COLUMN_ROLE": "Função", + "COLUMN_STATUS": "Situação", + "STATUS_ACTIVE": "Ativo", + "STATUS_PENDING": "Pendente", + "DELETE_MEMBER": "Apagar membro", + "RESEND": "Resend", + "SUCCESS_SEND_INVITATION": "Enviamos novamente um convite para '{{email}}'.", + "ERROR_SEND_INVITATION": "Nós não enviamos o convite.", + "SUCCESS_DELETE": "Nós apagamos {{message}}.", + "ERROR_DELETE": "Nós não fomos capaz de apagar {{message}}.", + "DEFAULT_DELETE_MESSAGE": "o convite para {{email}}" + }, + "DEFAULT_VALUES": { + "LABEL_POINTS": "Valores padrões para o seletor de pontos", + "LABEL_US": "Valor padrão para seletor de status da US", + "LABEL_TASK_STATUS": "Valor padrão para seletor de estatus de tarefa", + "LABEL_PRIORITY": "Valor padão para seletor de prioridade", + "LABEL_SEVERITY": "Valor padrão para seletor de severidade", + "LABEL_ISSUE_TYPE": "Valor padrão para seletor de tipo de caso", + "LABEL_ISSUE_STATUS": "Valor padrão para seletor de estatus de caso" + }, + "STATUS": { + "PLACEHOLDER_WRITE_STATUS_NAME": "Digite um nome para o novo estatus" + }, + "TYPES": { + "PLACEHOLDER_WRITE_NAME": "Escreva o nome do novo elemento" + }, + "US_STATUS": { + "ACTION_ADD_STATUS": "Adicionar novo estatus", + "IS_ARCHIVED_COLUMN": "Está arquivado?", + "WIP_LIMIT_COLUMN": "Limite WIP", + "PLACEHOLDER_WRITE_NAME": "Digite um nome para o novo estatus" + }, + "MENU": { + "TITLE": "Administrador", + "PROJECT": "Projeto", + "ATTRIBUTES": "Atributos", + "MEMBERS": "Membros", + "PERMISSIONS": "Permissões", + "INTEGRATIONS": "Integrações", + "PLUGINS": "Plugins" + }, + "SUBMENU_PROJECT_ATTRIBUTES": { + "TITLE": "Atributos" + }, + "SUBMENU_PROJECT_VALUES": { + "STATUS": "Situação", + "POINTS": "Pontos", + "PRIORITIES": "Prioridades", + "SEVERITIES": "Seriedades", + "TYPES": "Tipos", + "CUSTOM_FIELDS": "Campos personalizados" + }, + "SUBMENU_PROJECT_PROFILE": { + "TITLE": "Perfil do Projeto" + }, + "SUBMENU_ROLES": { + "TITLE": "Funções", + "ACTION_NEW_ROLE": "+ Nova Função", + "TITLE_ACTION_NEW_ROLE": "Adicionar nova função" + }, + "SUBMENU_THIDPARTIES": { + "TITLE": "Serviços" + } + }, + "USER": { + "PROFILE": { + "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", + "EDIT": "Editar Perfil", + "FOLLOW": "Seguir", + "CLOSED_US": "US Fechadas", + "PROJECTS": "Projetos", + "PROJECTS_EMPTY": "{{username}} ainda não tem projetos", + "CONTACTS": "Contatos", + "CONTACTS_EMPTY": "{{username}} ainda não tem nenhum contato", + "CURRENT_USER_CONTACTS_EMPTY": "Você não tem nenhum contato", + "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "As pessoas que trabalahm na Taiga serão seus contatos automaticamente", + "REPORT": "Reportar Abuso", + "TABS": { + "ACTIVITY_TAB": "Atividade", + "ACTIVITY_TAB_TITLE": "Exibir todas as atividade deste usuário", + "PROJECTS_TAB": "Projetos", + "PROJECTS_TAB_TITLE": "Listar todos projetos que este usuário é membro", + "LIKES_TAB": "Curtir", + "LIKES_TAB_TITLE": "Lista todos curtidas feitas por este usuário", + "VOTES_TAB": "Votos", + "VOTES_TAB_TITLE": "Listar todos os votos feitos por este usuário", + "WATCHED_TAB": "Observado", + "WATCHED_TAB_TITLE": "Exibe todos itens seguidos por este usuário", + "CONTACTS_TAB": "Contatos", + "CONTACTS_TAB_TITLE": "Exibe todos contatos feitos por este usuário" + } + }, + "PROFILE_SIDEBAR": { + "TITLE": "Seu perfil", + "DESCRIPTION": "Pessoas podem ver tudo o que você faz e seu desempenho. Adicione uma boa biografia para aprimorar sua informação.", + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Digite algo...", + "FILTER_TYPE_ALL": "Tudo", + "FILTER_TYPE_ALL_TITLE": "Mostrar tudo", + "FILTER_TYPE_PROJECTS": "Projetos", + "FILTER_TYPE_PROJECT_TITLES": "Mostrar somente projetos", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Mostrar apenas user stories", + "FILTER_TYPE_TASKS": "Tarefas", + "FILTER_TYPE_TASK_TITLES": "Mostrar apenas tarefas", + "FILTER_TYPE_ISSUES": "Casos", + "FILTER_TYPE_ISSUES_TITLE": "mostrar apenas problemas", + "EMPTY_TITLE": "It looks like there's nothing to show here." + } + }, + "PROJECT": { + "PAGE_TITLE": "{{projectName}}", + "WELCOME": "Bem vindo", + "SECTION_PROJECTS": "Projetos", + "HELP": "Reordene seus projetos para colocar no topo os mais usados.
Os 10 primeiros projetos aparecerão na lista de projetos da barra de navegação superior.", + "PRIVATE": "Projeto Privado", + "STATS": { + "PROJECT": "projetos
pontos", + "DEFINED": "pontos
definidos", + "ASSIGNED": "pontos
com designação", + "CLOSED": "pontos
fechados" + }, + "SECTION": { + "SEARCH": "Procurar", + "TIMELINE": "Linha do Tempo", + "BACKLOG": "Backlog", + "KANBAN": "Kanban", + "ISSUES": "Casos", + "WIKI": "Wiki", + "TEAM": "Time", + "MEETUP": "Reunião", + "ADMIN": "Administrador" + }, + "NAVIGATION": { + "SECTION_TITLE": "Seus projetos", + "PLACEHOLDER_SEARCH": "Procurar em...", + "ACTION_CREATE_PROJECT": "Criar projeto", + "ACTION_IMPORT_PROJECT": "Importar projeto", + "SEE_MORE_PROJECTS": "Ver mais projetos", + "TITLE_CREATE_PROJECT": "Criar projeto", + "TITLE_IMPORT_PROJECT": "Importar projeto", + "TITLE_PRVIOUS_PROJECT": "Mostrar projetos prévios", + "TITLE_NEXT_PROJECT": "Mostrar os próximos projetos", + "HELP_TITLE": "Página de Suporte do Taiga", + "HELP": "Ajuda", + "FEEDBACK_TITLE": "Enviar feedback", + "FEEDBACK": "Feedback", + "NOTIFICATIONS_TITLE": "Editar suas configurações de notificações", + "NOTIFICATIONS": "Notificações", + "ORGANIZATIONS_TITLE": "Edite suas organizações", + "ORGANIZATIONS": "Editar organizações", + "SETTINGS_TITLE": "Editar suas configurações", + "SETTINGS": "Configurações", + "VIEW_PROFILE_TITLE": "Ver perfil", + "VIEW_PROFILE": "Ver perfil", + "EDIT_PROFILE_TITLE": "Edit seu perfil", + "EDIT_PROFILE": "Editar Perfil", + "CHANGE_PASSWORD_TITLE": "Alterar senha", + "CHANGE_PASSWORD": "Alterar senha", + "DASHBOARD_TITLE": "Painel", + "DISCOVER_TITLE": "Descobrir projetos em destaques", + "DISCOVER": "Descubra", + "ACTION_REORDER": "Seguro e arraste para ordenar" + }, + "IMPORT": { + "TITLE": "Importando Projeto", + "UPLOADING_FILE": "Enviando arquivo de dump", + "DESCRIPTION": "Este processo pode levar algum tempo, mantenha a janela aberta por favor.", + "ASYNC_IN_PROGRESS_TITLE": "Nossos Oompa Loompas estão importando seu projeto", + "ASYNC_IN_PROGRESS_MESSAGE": "Este processo pode levar alguns minutos
Enviaremos um email a você assim que estiver pronto", + "UPLOAD_IN_PROGRESS_MESSAGE": "Enviado {{uploadedSize}} de {{totalSize}}", + "ERROR": "Nossos Oompa Loompas tiveram alguns problemas importando os dados do seu dump. Tente novamente.", + "ERROR_TOO_MANY_REQUEST": "Desculpe, nossos Oompa Loompas estão muito ocupados neste instante. Tente novamente em alguns minutos.", + "ERROR_MESSAGE": "Nossos Oompa Loompas tiveram alguns problemas ao importar seu arquivo de despejo: {{error_message}}", + "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) é muito pesado para nossos Oompa Loompas, tente algo menor que ({{maxFileSize}})", + "SYNC_SUCCESS": "Seu projeto foi importado com sucesso" + }, + "LIKE_BUTTON": { + "LIKE": "Curtir", + "LIKED": "Curtiu", + "UNLIKE": "Descurtir", + "BUTTON_TITLE": "Curtir ou descurtir este projeto", + "COUNTER_TITLE": "{total, plural, um{um fã} outro{# fãs}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Observar este projeto e definir política de notificação", + "WATCH": "Observar", + "WATCHING": "Observando", + "COUNTER_TITLE": "{total, plural, um{um observador} outro{#watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "receber todas notificações", + "NOTIFY_ALL_TITLE": "Receber todas notificações para este projeto", + "NOTIFY_INVOLVED": "Apenas envolvidos", + "NOTIFY_INVOLVED_TITLE": "Receber notificações apenas enquanto você estiver envolvido", + "UNWATCH": "Deixar de Observar", + "UNWATCH_TITLE": "Deixar de observar este projeto" + } + } + }, + "LIGHTBOX": { + "DELETE_ACCOUNT": { + "SECTION_NAME": "Apagar conta no Taiga", + "CONFIRM": "Você tem certeza que quer apagar sua conta Taiga?", + "SUBTITLE": "Nós vamos sentir sua falta! :-(", + "NEWSLETTER_LABEL_TEXT": "Eu não quero receber mais os informativos" + }, + "DELETE_PROJECT": { + "TITLE": "Apagar projeto", + "QUESTION": "Você tem certeza que quer apagar este projeto?", + "SUBTITLE": "Todas as informações do projeto (estórias de uso, tarefas, casos, sprints e páginas de wiki) serão perdidos! :-(", + "CONFIRM": "Sim, eu tenho certeza" + }, + "ASSIGNED_TO": { + "SELECT": "Selecionar assinalados para", + "SEARCH": "Procurar por usuários" + }, + "ADD_MEMBER": { + "TITLE": "Novo Membro", + "HELP_TEXT": "Se os usuários já estiverem registrados no Taiga, eles serão adicionados automaticamente. Caso contrário, eles receberão um convite." + }, + "CREATE_ISSUE": { + "TITLE": "Adicionar caso" + }, + "FEEDBACK": { + "TITLE": "Diga-nos algo...", + "COMMENT": "...um problema, algumas sugestões, algo legal... ou até seu pior pesadelo com Taiga", + "ACTION_SEND": "Enviar feedback" + }, + "SEARCH": { + "TITLE": "Procurar", + "PLACEHOLDER_SEARCH": "O que você está procurando?" + }, + "ADD_EDIT_SPRINT": { + "TITLE": "Novo Sprint", + "PLACEHOLDER_SPRINT_NAME": "nome do Sprint", + "PLACEHOLDER_SPRINT_START": "Inicio Estimado", + "PLACEHOLDER_SPRINT_END": "Estimativa de Termino", + "ACTION_DELETE_SPRINT": "Você quer apagar este Sprint?", + "TITLE_ACTION_DELETE_SPRINT": "apagar sprint", + "LAST_SPRINT_NAME": "último sprint é {{lastSprint}} ;-) " + }, + "CREATE_EDIT_TASK": { + "TITLE": "Nova tarefa", + "PLACEHOLDER_SUBJECT": "Um assunto da tarefa", + "PLACEHOLDER_STATUS": "Situação da Tarefa", + "OPTION_UNASSIGNED": "Não assinalado", + "PLACEHOLDER_SHORT_DESCRIPTION": "Escreva uma curta descrição", + "ACTION_EDIT": "Editar tarefa" + }, + "CREATE_EDIT_US": { + "TITLE": "Nova EU", + "PLACEHOLDER_DESCRIPTION": "Por favor, adicione um texto descritivo para ajudar outras pessoas a entenderem melhor esta EU", + "NEW_US": "Nova user story", + "EDIT_US": "Editar user story" + }, + "DELETE_SPRINT": { + "TITLE": "Apagar Sprint" + }, + "CREATE_MEMBER": { + "PLACEHOLDER_INVITATION_TEXT": "(Opcional) Adicione uma mensagem de texto ao convite. Diga algo animador para os novos membros ;-)", + "PLACEHOLDER_TYPE_EMAIL": "Digite um Email" + } + }, + "US": { + "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Estado: {{userStoryStatus }}. Completos {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} de {{userStoryTotalTasks}} tarefas encerradas). Pontos: {{userStoryPoints}}. Descrição: {{userStoryDescription}}", + "SECTION_NAME": "Detalhes de User Story", + "LINK_TASKBOARD": "Quadro de Tarefas", + "TITLE_LINK_TASKBOARD": "Ir para o quadro de tarefas", + "TOTAL_POINTS": "total de pontos", + "ADD": "+ Adicionar uma nova User Story", + "ADD_BULK": "Adicionar User Stories em lote", + "PROMOTED": "Esta estória de uso foi promovida do caso:", + "TITLE_LINK_GO_TO_ISSUE": "Ir para caso", + "EXTERNAL_REFERENCE": "Esta EU foi criada de", + "GO_TO_EXTERNAL_REFERENCE": "Ir para a origem", + "BLOCKED": "Esta user story está bloqueada", + "PREVIOUS": "user story anterior", + "NEXT": "proxima user story", + "TITLE_DELETE_ACTION": "Apagar user story", + "LIGHTBOX_TITLE_BLOKING_US": "Bloqueando user story", + "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} tarefas completas", + "ASSIGN": "Atribuir User Story", + "NOT_ESTIMATED": "Não estimado", + "TOTAL_US_POINTS": "Pontos totais de US", + "FIELDS": { + "TEAM_REQUIREMENT": "Requisitos da Equipe", + "CLIENT_REQUIREMENT": "Requisitos do Cliente", + "FINISH_DATE": "Data de término" + } + }, + "COMMENTS": { + "DELETED_INFO": "Comentário apagado por {{user}} em {{date}}", + "TITLE": "Comentários", + "COMMENT": "Comentário", + "TYPE_NEW_COMMENT": "Escreva um novo comentário aqui", + "SHOW_DELETED": "Mostrar comentários apagados", + "HIDE_DELETED": "Esconder comentário apagado", + "RESTORE": "Restaurar comentário" + }, + "ACTIVITY": { + "SHOW_ACTIVITY": "Exibir atividade", + "DATETIME": "DD MMM YYYY HH:mm", + "SHOW_MORE": "+ Mostrar entradas anteriores (mais {{showMore}})", + "TITLE": "Atividade", + "REMOVED": "removido", + "ADDED": "adicionado", + "US_POINTS": "pontos EU ({{name}})", + "NEW_ATTACHMENT": "novo anexo", + "DELETED_ATTACHMENT": "apagar anexo", + "UPDATED_ATTACHMENT": "anexo atualizado {{filename}}", + "DELETED_CUSTOM_ATTRIBUTE": "atributo personalizado apagado", + "SIZE_CHANGE": "Feito {size, plural, one{one change} other{# changes}}", + "VALUES": { + "YES": "sim", + "NO": "não", + "EMPTY": "vazio", + "UNASSIGNED": "não atribuído" + }, + "FIELDS": { + "SUBJECT": "assunto", + "NAME": "nome", + "DESCRIPTION": "descrição", + "CONTENT": "conteúdo", + "STATUS": "Situação", + "IS_CLOSED": "está fechado", + "FINISH_DATE": "Data de término", + "TYPE": "Digite", + "PRIORITY": "prioridade", + "SEVERITY": "Severidade", + "ASSIGNED_TO": "Assinado à", + "WATCHERS": "Observadores", + "MILESTONE": "sprint", + "USER_STORY": "user story", + "PROJECT": "projeto", + "IS_BLOCKED": "está bloqueada", + "BLOCKED_NOTE": "Nota de bloqueio", + "POINTS": "pontos", + "CLIENT_REQUIREMENT": "requisitos do cliente", + "TEAM_REQUIREMENT": "requisitos da equipe", + "IS_IOCAINE": "É Iocaine", + "TAGS": "tags", + "ATTACHMENTS": "anexos", + "IS_DEPRECATED": "está obsoleto", + "ORDER": "ordem", + "BACKLOG_ORDER": "requisição do backlog", + "SPRINT_ORDER": "ordem de sprint ", + "KANBAN_ORDER": "pedido kanban", + "TASKBOARD_ORDER": "Ordem de quadro de tarefa", + "US_ORDER": "requisição US " + } + }, + "BACKLOG": { + "PAGE_TITLE": "Backlog - {{projectName}}", + "PAGE_DESCRIPTION": "O painel de backlog, com user stories e sprints do projeto {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Backlog", + "CUSTOMIZE_GRAPH": "Personalize seu gráficos de backlog", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Administrador", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", + "MOVE_US_TO_CURRENT_SPRINT": "Mover para Sprint Corrente", + "SHOW_FILTERS": "Mostrar filtros", + "SHOW_TAGS": "Exibir tags", + "EMPTY": "Seu backlog está vazio!", + "CREATE_NEW_US": "Criar uma nova EU", + "CREATE_NEW_US_EMPTY_HELP": "Você talvez queira criar uma nova user story", + "EXCESS_OF_POINTS": "Excesso de pontos", + "PENDING_POINTS": "Pontos Pendentes", + "CLOSED_POINTS": "fechado", + "COMPACT_SPRINT": "Sprint Resumido", + "GO_TO_TASKBOARD": "Ir ao quadro de tarefas de {{::name}}", + "EDIT_SPRINT": "Editar Sprint", + "TOTAL_POINTS": "total", + "STATUS_NAME": "Nome de Situação", + "SORTABLE_FILTER_ERROR": "Você não pode jogar sobre o backlog quando filtros estão abertos", + "DOOMLINE": "Escopo do projeto [Doomline]", + "CHART": { + "XAXIS_LABEL": "Sprintes", + "YAXIS_LABEL": "Pontos", + "OPTIMAL": "Ideal de pontos pendentes para o sprint \"{{sprintName}}\" deve ser {{value}}", + "REAL": "Real pending points for sprint \"{{sprintName}}\" is {{value}}", + "INCREMENT_TEAM": "Pontos incrementados pelos requerimentos do time para o sprint \"{{sprintName}}\" é {{value}}", + "INCREMENT_CLIENT": "Pontos incrementados pelos requerimentos do cliente para o sprint \"{{sprintName}}\" é {{value}}" + }, + "TAGS": { + "TOGGLE": "Alternar visibilidade das tags", + "SHOW": "Exibir tags", + "HIDE": "Esconder tags" + }, + "TABLE": { + "COLUMN_US": "User Stories", + "TITLE_COLUMN_POINTS": "Selecionar exibição por função" + }, + "SPRINT_SUMMARY": { + "TOTAL_POINTS": "pontos
totais", + "COMPLETED_POINTS": "pontos
completados", + "OPEN_TASKS": "tasks
abertas", + "CLOSED_TASKS": "tarefas
fechadas", + "IOCAINE_DOSES": "iocaine
doses", + "SHOW_STATISTICS_TITLE": "Mostrar estatísticas", + "TOGGLE_BAKLOG_GRAPH": "Mostrar/Esconder gráfico burndown" + }, + "SUMMARY": { + "PROJECT_POINTS": "pontos do
projeto", + "DEFINED_POINTS": "pontos
definidos", + "CLOSED_POINTS": "pontos
fechados", + "POINTS_PER_SPRINT": "pontos /
sprint" + }, + "FILTERS": { + "TOGGLE": "Alternar visibilidade de filtros", + "TITLE": "Filtros", + "REMOVE": "Remover filtros", + "HIDE": "Esconder Filtros", + "SHOW": "Mostrar Filtros", + "FILTER_CATEGORY_STATUS": "Situação", + "FILTER_CATEGORY_TAGS": "Tags" + }, + "SPRINTS": { + "TITLE": "SPRINTS", + "DATE": "DD MMM YYYY", + "LINK_TASKBOARD": "Sprint quadro de tarefas", + "TITLE_LINK_TASKBOARD": "ir para quadro de tarefas de \"{{name}}\"", + "NUMBER_SPRINTS": "
sprints", + "EMPTY": "VOCÊ NÃO POSSUI SPRINTS", + "WARNING_EMPTY_SPRINT": "Solte aqui Estórias do seu backlog para iniciar uma nova sprint", + "TITLE_ACTION_NEW_SPRINT": "Add nova sprint", + "TEXT_ACTION_NEW_SPRINT": "Você poderá querer criar uma nova sprint em seu projeto", + "ACTION_SHOW_CLOSED_SPRINTS": "Mostre os sprints fechados", + "ACTION_HIDE_CLOSED_SPRINTS": "Esconder sprints fechados" + } + }, + "ERROR": { + "TEXT1": "Alguma coisa aconteceu e nossos Oompa Loompas estão trabalhando nisso.", + "NOT_FOUND": "Não encontrado", + "NOT_FOUND_TEXT": "Erro 404. A página que você procura não existe mais. Você pode voltar para a página do TAIGA e ver se consegue encontrar o que está procurando.", + "PERMISSION_DENIED": "Permissão negada", + "PERMISSION_DENIED_TEXT": "Você não tem permissão de acesso a esta página.", + "VERSION_ERROR": "Alguém dentro da Taiga mudou isso antes e nosso Oompa Loompa não podem aplicar suas alterações. Recarregue a página e aplique-as novamente (elas serão perdidas, agora)." + }, + "TASKBOARD": { + "PAGE_TITLE": "{{sprintName}} - Quadro de Tarefas do Sprint - {{projectName}}", + "PAGE_DESCRIPTION": "Sprint {{sprintName}} (de{{startDate}} até {{endDate}}) para {{projectName}}. Completos {{completedPercentage}}% ({{completedPoints}} de {{totalPoints}} pontos). {{openTasks}} tarefas abertas de {{totalTasks}}.", + "SECTION_NAME": "Quadro de Tarefas", + "TITLE_ACTION_ADD": "Adicionar uma nova Tarefa", + "TITLE_ACTION_ADD_BULK": "Adicionar algumas tarefas em lote", + "TITLE_ACTION_ASSIGN": "Assinalar tarefa", + "TITLE_ACTION_EDIT": "Editar tarefa", + "PLACEHOLDER_CARD_TITLE": "Isto pode ser uma atividade", + "PLACEHOLDER_CARD_TEXT": "Separe Estórias em atividades para rastrear separadamente.", + "TABLE": { + "COLUMN": "user story", + "TITLE_ACTION_FOLD": "Recolher coluna", + "TITLE_ACTION_UNFOLD": "Abrir coluna", + "TITLE_ACTION_FOLD_ROW": "Guardar Linha", + "TITLE_ACTION_UNFOLD_ROW": "Abrir linha", + "FIELD_POINTS": "pontos", + "ROW_UNASSIGED_TASKS_TITLE": "Tarefas não atribuídas" + }, + "CHARTS": { + "XAXIS_LABEL": "Dias", + "YAXIS_LABEL": "Pontos", + "OPTIMAL": "Pontos pendentes ideais para o dia {{formattedDate}} deve ser {{roundedValue}}", + "REAL": "\"Pontos pendentes reais para o dia {{formattedDate}} são {{roundedValue}}\"", + "DATE": "DD MMM YYYY" + } + }, + "TASK": { + "PAGE_TITLE": "{{taskSubject}} - Tarefa {{taskRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Estado: {{taskStatus }}. Descrição: {{taskDescription}}", + "SECTION_NAME": "Detalhes da Tarefa", + "LINK_TASKBOARD": "Quadro de Tarefas", + "TITLE_LINK_TASKBOARD": "Ir para o quadro de tarefas", + "PLACEHOLDER_SUBJECT": "Digite um novo titulo para tarefa", + "TITLE_SELECT_STATUS": "Nome de Situação", + "OWNER_US": "Esta tarefa pertence a ", + "TITLE_LINK_GO_OWNER": "Ir para user story", + "ORIGIN_US": "Essa tarefa foi criada a partir de", + "TITLE_LINK_GO_ORIGIN": "Ir para user story", + "BLOCKED": "Esta tarefa está bloqueada", + "PREVIOUS": "tarefa anterior", + "NEXT": "nova tarefa", + "TITLE_DELETE_ACTION": "Apagar Tarefa", + "LIGHTBOX_TITLE_BLOKING_TASK": "Tarefa bloqueadora", + "FIELDS": { + "MILESTONE": "Sprint", + "USER_STORY": "User story", + "IS_IOCAINE": "É Iocaine" + }, + "ACTION_IOCAINE": "Iocaine", + "TITLE_ACTION_IOCAINE": "Se sentindo sobrecarregado por uma tarefa? Assegure-se de que os outros saibam disso clicando em Iocaine quando estiver editando a tarefa. É possível se tornar imune a essse veneno mortal (fictício) consumindo pequenas quantidades ao longo do tempo, assim como é possível ficar melhor no que faz, ocasionalmente, por assumir desafios extras!" + }, + "NOTIFICATION": { + "OK": "Tudo está ok", + "WARNING": "Oops, aconteceu algo...", + "WARNING_TEXT": "Nossos Oompa Loompas estão tristes, suas alterações não foram salvas.", + "SAVED": "Nossos Oompa Loompas salvaram todas as suas mudanças!", + "CLOSE": "Fechar Notificação", + "MAIL": "Notificações por Email", + "ASK_DELETE": "Você tem certeza que quer apagar?" + }, + "CANCEL_ACCOUNT": { + "TITLE": "Cancelar sua conta", + "SUBTITLE": "Sentimos por você estar saindo. Esperamos que tenha gostado :)", + "PLACEHOLDER_INPUT_TOKEN": "cancelar o token da conta", + "ACTION_LEAVING": "Sim, estou indo embora!", + "SUCCESS": "Nossos Oompa Loompas removeram sua conta" + }, + "CHANGE_EMAIL_FORM": { + "TITLE": "Mudar seu email", + "SUBTITLE": "Só mais um clique e seu email será atualizado!", + "PLACEHOLDER_INPUT_TOKEN": "mudar token do email", + "ACTION_CHANGE_EMAIL": "Mudar email", + "SUCCESS": "Nossos Oompa Loompas atualizaram seu email" + }, + "ISSUES": { + "PAGE_TITLE": "Casos - {{projectName}}", + "PAGE_DESCRIPTION": "O painel de casos do projeto {{projectName}}: {{projectDescription}}", + "LIST_SECTION_NAME": "Casos", + "SECTION_NAME": "Detalhes do caso", + "ACTION_NEW_ISSUE": "+ NOVO CASO", + "ACTION_PROMOTE_TO_US": "Promover para User Story", + "PLACEHOLDER_FILTER_NAME": "Digite o nome do filtro e pressione Enter", + "PROMOTED": "Esse caso foi promovido para user strory", + "EXTERNAL_REFERENCE": "Esse caso foi criado a partir de", + "GO_TO_EXTERNAL_REFERENCE": "Ir para a origem", + "BLOCKED": "Esse caso está bloqueado", + "TITLE_PREVIOUS_ISSUE": "caso anterior", + "TITLE_NEXT_ISSUE": "próximo caso", + "ACTION_DELETE": "Caso apagado", + "LIGHTBOX_TITLE_BLOKING_ISSUE": "Caso que está bloqueando", + "FIELDS": { + "PRIORITY": "Prioridade", + "SEVERITY": "Severidade", + "TYPE": "Tipo" + }, + "CONFIRM_PROMOTE": { + "TITLE": "Promover esse caso para nova user story", + "MESSAGE": "Você tem certeza que deseja criar uma nova HU para esse caso?" + }, + "FILTERS": { + "TITLE": "Filtros", + "INPUT_SEARCH_PLACEHOLDER": "Assunto ou ref", + "TITLE_ACTION_SEARCH": "Procurar", + "ACTION_SAVE_CUSTOM_FILTER": "salve como filtro personalizado", + "BREADCRUMB": "Filtros", + "TITLE_BREADCRUMB": "Filtros", + "CATEGORIES": { + "TYPE": "Tipo", + "STATUS": "Situação", + "SEVERITY": "Severidade", + "PRIORITIES": "Prioridades", + "TAGS": "Tags", + "ASSIGNED_TO": "Atribuído a", + "CREATED_BY": "Criado por", + "CUSTOM_FILTERS": "Filtros personalizados" + }, + "CONFIRM_DELETE": { + "TITLE": "Apagar filtro personalizado", + "MESSAGE": "O filtro personalizado '{{customFilterName}}'" + } + }, + "TABLE": { + "COLUMNS": { + "TYPE": "Tipo", + "SEVERITY": "Severidade", + "PRIORITY": "Prioridade", + "SUBJECT": "Assunto", + "VOTES": "Votos", + "STATUS": "Situação", + "CREATED": "Criado", + "ASSIGNED_TO": "Atribuído a" + }, + "TITLE_ACTION_CHANGE_STATUS": "Mudar status", + "TITLE_ACTION_ASSIGNED_TO": "Atribuído a", + "BLOCKED": "Bloqueado", + "EMPTY": { + "TITLE": "Não há casos para reportar :-)", + "SUBTITLE": "Você encontrou um caso ?" + } + } + }, + "ISSUE": { + "PAGE_TITLE": "{{issueSubject}} - caso {{issueRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Estado: {{issueStatus }}. Tipo: {{issueType}}, Prioridade: {{issuePriority}}. severidade: {{issueSeverity}}. Descrição: {{issueDescription}}" + }, + "KANBAN": { + "PAGE_TITLE": "Kanban - {{projectName}}", + "PAGE_DESCRIPTION": "O painel do kanban, contendo user stories do projeto {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Kanban", + "TITLE_ACTION_FOLD": "Recolher coluna", + "TITLE_ACTION_UNFOLD": "Abrir coluna", + "TITLE_ACTION_FOLD_CARDS": "Guardar cartões", + "TITLE_ACTION_UNFOLD_CARDS": "Mostrar cartões", + "TITLE_ACTION_ADD_US": "Adicionar Nova User Story", + "TITLE_ACTION_ADD_BULK": "Adicionar novo lote", + "ACTION_SHOW_ARCHIVED": "Mostrar arquivados", + "ACTION_HIDE_ARCHIVED": "esconder arquivados", + "HIDDEN_USER_STORIES": "As user stories nesse status estão escondidas por padrão", + "ARCHIVED": "Você arquivou", + "UNDO_ARCHIVED": "Pegue e arraste novamente para desfazer", + "PLACEHOLDER_CARD_TITLE": "Estas são suas Estórias de Usuário ", + "PLACEHOLDER_CARD_TEXT": "Estórias deverá também ter sub-atividades para separar os requerimentos" + }, + "SEARCH": { + "PAGE_TITLE": "Buscar - {{projectName}}", + "PAGE_DESCRIPTION": "Busque qualquer coisa, user stories, casos, tarefas, ou páginas da wiki, no projeto {{projectName}}: {{projectDescription}}", + "FILTER_USER_STORIES": "User Stories", + "FILTER_ISSUES": "casos", + "FILTER_TASKS": "Tarefas", + "FILTER_WIKI": "Paginas Wiki", + "PLACEHOLDER_SEARCH": "Procurar em...", + "TITLE_ACTION_SEARCH": "procurar", + "EMPTY_TITLE": "Parece que nada foi encontrado com o critério de busca", + "EMPTY_DESCRIPTION": "Talvez tente uma das abas acima ou busque novamente" + }, + "TEAM": { + "PAGE_TITLE": "Equipe - {{projectName}}", + "PAGE_DESCRIPTION": "O painel do time irá exibir todos os membros envolvidos no projeto {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Time", + "APP_TITLE": "EQUIPE - {{projectName}}", + "PLACEHOLDER_INPUT_SEARCH": "Procurar pelo nome completo...", + "COLUMN_MR_WOLF": "Sr. Wolf", + "EXPLANATION_COLUMN_MR_WOLF": "Fechar caso", + "COLUMN_IOCAINE": "Bebedor de Iocaine", + "EXPLANATION_COLUMN_IOCAINE": "Doses de Iocaine ingeridas", + "COLUMN_CERVANTES": "Pero Vaz de Caminha", + "EXPLANATION_COLUMN_CERVANTES": "Páginas wiki editadas", + "COLUMN_BUG_HUNTER": "Caçador de bugs", + "EXPLANATION_COLUMN_BUG_HUNTER": "Caso reportado", + "COLUMN_NIGHT_SHIFT": "Periodo Noturno", + "EXPLANATION_COLUMN_NIGHT_SHIFT": "Tarefa fechada", + "COLUMN_TOTAL_POWER": "Total de poder", + "EXPLANATION_COLUMN_TOTAL_POWER": "Pontuação Total", + "SECTION_TITLE_TEAM": "Equipe >", + "SECTION_FILTER_ALL": "Tudo", + "CONFIRM_LEAVE_PROJECT": "Você tem certeza que quer deixar o projeto?", + "ACTION_LEAVE_PROJECT": "Deixar este projeto" + }, + "USER_SETTINGS": { + "AVATAR_MAX_SIZE": "[Tamanho máximo: {{maxFileSize}}]", + "MENU": { + "SECTION_TITLE": "Configurações de usuário", + "USER_PROFILE": "Perfil do Usuário", + "CHANGE_PASSWORD": "Alterar senha", + "EMAIL_NOTIFICATIONS": "Notificações por email" + }, + "NOTIFICATIONS": { + "SECTION_NAME": "Notificações por email", + "COLUMN_PROJECT": "Projeto", + "COLUMN_RECEIVE_ALL": "Receber todos", + "COLUMN_ONLY_INVOLVED": "Apenas envolvido", + "COLUMN_NO_NOTIFICATIONS": "Sem notificações", + "OPTION_ALL": "Tudo", + "OPTION_INVOLVED": "Envolvido", + "OPTION_NONE": "Nada" + }, + "POPOVER": { + "USER_PROFILE": "Perfil do Usuário", + "CHANGE_PASSWORD": "Alterar Senha", + "NOTIFICATIONS": "Notificações", + "FEEDBACK": "Feedback", + "TITLE_AVATAR": "Preferências do Usuário" + } + }, + "USER_PROFILE": { + "IMAGE_HELP": "A imagem será redimensionada para 80x80px", + "ACTION_CHANGE_IMAGE": "Alterar", + "ACTION_USE_GRAVATAR": "Usar imagem do gravatar", + "ACTION_DELETE_ACCOUNT": "Apagar conta Taiga", + "CHANGE_EMAIL_SUCCESS": "Verifique sua caixa de entrada!
Enviamos um e-mail para sua conta
com as instruções para definir seu novo endereço", + "CHANGE_PHOTO": "Mudar foto", + "FIELD": { + "USERNAME": "Nome do usuário", + "EMAIL": "Email", + "FULL_NAME": "Nome completo", + "PLACEHOLDER_FULL_NAME": "Preencha seu nome completo (ex. Íñigo Montoya)", + "BIO": "Bio (max. 210 caracteres)", + "PLACEHOLDER_BIO": "Diga algo sobre você", + "LANGUAGE": "Idioma", + "LANGUAGE_DEFAULT": "-- usar idioma padrão --", + "THEME": "Tema", + "THEME_DEFAULT": "-- user tema padrão --" + } + }, + "WIZARD": { + "SECTION_TITLE_CHOOSE_TEMPLATE": "Escolher um template", + "CHOOSE_TEMPLATE_TEXT": "Qual template se encaixa melhor no seu projeto?", + "SECTION_TITLE_CREATE_PROJECT": "Criar Projeto", + "CREATE_PROJECT_TEXT": "Novo em folha. Tão excitante!", + "PROGRESS_TEMPLATE_SELECTION": "Seleção de template", + "PROGRESS_NAME_DESCRIPTION": "Nome e descrição" + }, + "WIKI": { + "PAGE_TITLE": "{{wikiPageName}} - Wiki - {{projectName}}", + "PAGE_DESCRIPTION": "Ultima edição em {{lastModifiedDate}} ({{totalEditions}} total de edições) Conteúdo: {{ wikiPageContent }}", + "DATETIME": "DD MMM YYYY HH:mm", + "PLACEHOLDER_PAGE": "Escreve sua página wiki", + "REMOVE": "Remover essa página wiki", + "DELETE_LIGHTBOX_TITLE": "Apagar página Wiki", + "NAVIGATION": { + "SECTION_NAME": "Links", + "ACTION_ADD_LINK": "Adicionar link" + }, + "SUMMARY": { + "TIMES_EDITED": "vezes
editadas", + "LAST_EDIT": "última
edição", + "LAST_MODIFICATION": "ultima modificação" + } + }, + "HINTS": { + "SECTION_NAME": "Sugestões", + "LINK": "Se você quer saber como usar visite nossa página de suporte", + "LINK_TITLE": "Visite nossa página de suporte", + "HINT1_TITLE": "Você sabia que você pode importar e exportar projetos?", + "HINT1_TEXT": "Isso permite você extrair todo o seu conteúdo de um Taiga e mover para outro.", + "HINT2_TITLE": "Você sabia que pode criar campos personalizados?", + "HINT2_TEXT": "Times podem agora criar campos personalizados flexíveis como entrar valores relevantes especificamente para seus escopos de trabalho", + "HINT3_TITLE": "Reordenar seus projetos para evidenciar os mais relevantes para você", + "HINT3_TEXT": "The top 10 project will be in your top bar direct access\nOs 10 projetos com maior destaque estarão na sua barra de acesso direto", + "HINT4_TITLE": "Você esqueceu no que está trabalhando?", + "HINT4_TEXT": "Não se preocupe, no seu painel você vai encontrar suas tarefas abertas, casos, e user stories na ordem que você trabalha neles." + }, + "TIMELINE": { + "UPLOAD_ATTACHMENT": "{{username}} adicionou um novo anexo em {{obj_name}}", + "US_CREATED": "{{username}} criou uma nova US {{obj_name}} em {{project_name}}", + "ISSUE_CREATED": "{{username}} criou um novo caso {{obj_name}} em {{project_name}}", + "TASK_CREATED": "{{username}} criou uma nova tarefa {{obj_name}} em {{project_name}}", + "TASK_CREATED_WITH_US": "{{username}} criou nova tarefa {{obj_name}} em {{project_name}} que pertence a US {{us_name}}", + "WIKI_CREATED": "{{username}} criou uma página wiki {{obj_name}} em {{project_name}}", + "MILESTONE_CREATED": "{{username}} criou um novo sprint {{obj_name}} em {{project_name}}", + "NEW_PROJECT": "{{username}} criou o projeto {{project_name}}", + "MILESTONE_UPDATED": "{{username}} atualizou o sprint {{obj_name}}", + "US_UPDATED": "{{username}} atualizou o atributo \"{{field_name}}\" da US {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} atualizou o trabalho \"{{field_name}}\" da US {{obj_name}} para {{new_value}}", + "US_UPDATED_POINTS": "{{username}} atualizou pontos de '{{role_name}}' da US {{obj_name}} para {{new_value}}", + "ISSUE_UPDATED": "{{username}} atualizou o atributo \"{{field_name}}\" do caso {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} atualizou o atributo \"{{field_name}}\" do caso {{obj_name}} para {{new_value}}", + "TASK_UPDATED": "{{username}} atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} para {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} Atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} para {{new_value}}", + "TASK_UPDATED_WITH_US": "{{username}} atualizou o atributo \"{{field_name}}\" da tarefa{{obj_name}} que pertence a US {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} atualizou o atributo \"{{field_name}}\" da tarefa {{obj_name}} que pertence a US {{us_name}} para {{new_value}}", + "WIKI_UPDATED": "{{username}} atualizou a página wiki {{obj_name}}", + "NEW_COMMENT_US": "{{username}} comentou na US {{obj_name}}", + "NEW_COMMENT_ISSUE": "{{username}} comentou no issue {{obj_name}}", + "NEW_COMMENT_TASK": "{{username}} comentou na tarefa {{obj_name}}", + "NEW_MEMBER": "{{project_name}} tem um membro novo", + "US_ADDED_MILESTONE": "{{username}} adicionou a US {{obj_name}} a {{sprint_name}}", + "US_MOVED": "{{username}} moveu a US {{obj_name}}", + "US_REMOVED_FROM_MILESTONE": "{{username}} adicionou a US {{obj_name}} ao backlog", + "BLOCKED": "{{username}} bloqueou {{obj_name}}", + "UNBLOCKED": "{{username}} desbloqueou {{obj_name}}", + "NEW_USER": "{{username}} ingressou no taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "Um app externo requer autenticação", + "PAGE_DESCRIPTION": "Um app externo requer autenticação", + "AUTHORIZATION_REQUEST": "Autorizar {{application}} a usar sua conta Taiga?", + "LOGIN_WITH_ANOTHER_USER": "Login com um outro usuário", + "AUTHORIZE_APP": "Autorizar app", + "CANCEL": "Cancelar" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Seu projeto", + "TEXT": "Bem-Vindo! Aqui você encontrará os projetos que você está envolvido. Temos para você, modelos de projetos para ajudar você a descobrir a potência do Taiga. " + }, + "STEP2": { + "TITLE": "Trabalhando em", + "TEXT": "Aqui você irá encontrar as Estórias do Usuário, Atividades e Problemas no qual você está trabalhando." + }, + "STEP3": { + "TITLE": "Observando", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "Você já está trabalhando com Taiga ;)" + }, + "STEP4": { + "TITLE": "Vamos começar", + "TEXT1": "Você pode iniciar por criando seu primeiro Projeto no Taiga", + "TEXT2": "Boa Sorte!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Sumário do projeto", + "TEXT1": "Aqui você verá o estado do seu projeto", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Backlog do Produto", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Sprints", + "TEXT": "Sprints são períodos curtos de tempo (geralmente 2 semanas) durante o qual trabalho especificado tem que ser completado e entregue." + }, + "STEP4": { + "TITLE": "User Stories", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Personalize seu fluxo de trabalho", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "Estórias do Usuário & Atividades", + "TEXT": "Estórias do Usuário são os requerimentos de alto nível. Você pode arrastar eles para diferentes colunas." + }, + "STEP3": { + "TITLE": "Adicionando User Stories", + "TEXT1": "Você poderá adicionar uma única Estória do Usuário (Ícone add US) ou um grupo delas (ícone de add em massa)", + "TEXT2": "Boa Sorte!" + } + } + } +} \ No newline at end of file diff --git a/app/locales/locale-ru.json b/app/locales/locale-ru.json new file mode 100644 index 00000000..9ac3bc0f --- /dev/null +++ b/app/locales/locale-ru.json @@ -0,0 +1,1411 @@ +{ + "COMMON": { + "YES": "Да", + "NO": "Нет", + "LOADING": "Пожалуйста, подождите...", + "LOADING_PROJECT": "Проект загружается...", + "DATE": "DD MMM YYYY", + "DATETIME": "DD MMM YYYY HH:mm", + "SAVE": "Сохранить", + "CANCEL": "Отмена", + "ACCEPT": "Принимаю", + "DELETE": "Удалить", + "CREATE": "Создать", + "ADD": "Добавить", + "COPY_TO_CLIPBOARD": "Копировать: Ctrl+C", + "EDIT": "Редактировать", + "DRAG": "Перетащите", + "TAG_LINE": "Ваш свободный, agile инструмент управления проектами с открытым исходным кодом", + "TAG_LINE_2": "Любит ваши проекты", + "BLOCK": "Блокировать", + "UNBLOCK": "Разблокировать", + "BLOCKED": "Заблокирован", + "CREATED_BY": "Создано {{fullDisplayName}}", + "FROM": "от", + "TO": "к", + "CLOSE": "закрыть", + "BLOCKED_NOTE": "Почему эта пользовательская история заблокирована?", + "BLOCKED_REASON": "Пожалуйста разъясните причину", + "GO_HOME": "Домой", + "PLUGINS": "Плагины", + "BETA": "Программа тестируется!", + "ONE_ITEM_LINE": "Один объект на строку...", + "NEW_BULK": "Добавить пакетно", + "RELATED_TASKS": "Связанные задачи", + "LOGOUT": "Выйти", + "EXTERNAL_USER": "внешний пользователь", + "GENERIC_ERROR": "Один из Умпа-Лумп говорит {{error}}.", + "IOCAINE_TEXT": "Чувствуете, что задание берет верх над вами? Дайте другим знать об этом, нажав на \"Иокаин\", когда редактируете задание. Возможно стать неуязвимым к этому (выдуманному) смертельном яду, потребляя небольшие количества время от времени, так же как возможно стать лучше в том, что вы делаете, временами беря на себя дополнительные препятствия!", + "CAPSLOCK_WARNING": "Будьте внимательны! Введённый текст состоит из заглавных букв, а регистр имеет значение.", + "FORM_ERRORS": { + "DEFAULT_MESSAGE": "Кажется, это значение некорректно.", + "TYPE_EMAIL": "Это значение должно быть корректным email-адресом.", + "TYPE_URL": "Это значение должно быть корректным URL-адресом.", + "TYPE_URLSTRICT": "Это значение должно быть корректным URL-адресом.", + "TYPE_NUMBER": "Это значение должно быть правильным числом", + "TYPE_DIGITS": "Это значение должно состоять из цифр.", + "TYPE_DATEISO": "Это значение должно быть корректной датой (в формате ГГГГ-ММ-ДД).", + "TYPE_ALPHANUM": "Это значение должно состоять только из букв и цифр.", + "TYPE_PHONE": "Это значение должно быть корректным номером телефона.", + "NOTNULL": "Это значение не должно быть пустым.", + "NOT_BLANK": "Это значение не должно быть пустым.", + "REQUIRED": "Это значение обязательно.", + "REGEXP": "Кажется, это значение некорректно.", + "MIN": "Это значение должно быть больше или равно %s.", + "MAX": "Значение должно быть меньше либо равно %s.", + "RANGE": "Это значение должно быть между %s и %s.", + "MIN_LENGTH": "Это значение слишком короткое. Оно должно иметь %s символов или больше.", + "MAX_LENGTH": "Это значение слишком длинное. Оно должно иметь %s символов или меньше.", + "RANGE_LENGTH": "Длина этого значения некорректна. Оно должно быть от %s до %s символов в длину.", + "MIN_CHECK": "Вы должны выбрать хотя бы %s вариантов.", + "MAX_CHECK": "Вы должны выбрать %s вариантов или меньше.", + "RANGE_CHECK": "Вы должны выбрать от %s до %s вариантов.", + "EQUAL_TO": "Это значение должно быть таким же." + }, + "PICKERDATE": { + "FORMAT": "DD MMM YYYY", + "IS_RTL": "false", + "FIRST_DAY_OF_WEEK": "1", + "PREV_MONTH": "Предыдущий Месяц", + "NEXT_MONTH": "Следующий Месяц", + "MONTHS": { + "JAN": "Январь", + "FEB": "Февраль", + "MAR": "Март", + "APR": "Апрель", + "MAY": "Май", + "JUN": "Июнь", + "JUL": "Июль", + "AUG": "Август", + "SEP": "Сентябрь", + "OCT": "Октябрь", + "NOV": "Ноябрь", + "DEC": "Декабрь" + }, + "WEEK_DAYS": { + "SUN": "Воскресенье", + "MON": "Понедельник", + "TUE": "Вторник", + "WED": "Среда", + "THU": "Черверг", + "FRI": "Пятница", + "SAT": "Суббота" + }, + "WEEK_DAYS_SHORT": { + "SUN": "Вск", + "MON": "Пнд", + "TUE": "Втр", + "WED": "Срд", + "THU": "Чтв", + "FRI": "Птн", + "SAT": "Сбт" + } + }, + "SEE_USER_PROFILE": "Посмотреть профиль {{username }}", + "USER_STORY": "Пользовательская история", + "TASK": "Задача", + "ISSUE": "Запрос", + "TAGS": { + "PLACEHOLDER": "Назначьте тэг", + "DELETE": "Удалить тэг", + "ADD": "Добавить тэг" + }, + "DESCRIPTION": { + "EMPTY": "Пустые места удручают... не стесняйтесь, напишите описание...", + "NO_DESCRIPTION": "Описания ещё нет" + }, + "FIELDS": { + "SUBJECT": "Тема", + "NAME": "Имя", + "URL": "URL", + "DESCRIPTION": "Описание", + "VALUE": "Значение", + "SLUG": "Ссылочное имя", + "COLOR": "Цвет", + "IS_CLOSED": "Закрыто?", + "STATUS": "Статус", + "TYPE": "Тип", + "SEVERITY": "Важность", + "PRIORITY": "Приоритет", + "ASSIGNED_TO": "Назначено", + "POINTS": "Очки", + "BLOCKED_NOTE": "Пояснение блокировки", + "IS_BLOCKED": "заблокирован", + "REF": "Ссылка", + "VOTES": "Голоса" + }, + "ROLES": { + "ALL": "Все" + }, + "ASSIGNED_TO": { + "NOT_ASSIGNED": "Не назначен", + "DELETE_ASSIGNMENT": "Удалить назначение", + "REMOVE_ASSIGNED": "Удалить назначение", + "TOO_MANY": "...слишком много пользователей, продолжайте фильтровать", + "CONFIRM_UNASSIGNED": "Вы уверены что не хотите назначить ответственного?", + "TITLE_ACTION_EDIT_ASSIGNMENT": "Изменить назначение" + }, + "STATUS": { + "CLOSED": "Закрыт", + "OPEN": "Открыть" + }, + "WATCHERS": { + "ADD": "Добавить наблюдателей", + "TITLE_ADD": "Добавить участника проекта к списку наблюдателей", + "DELETE": "Удалить наблюдателя", + "TITLE_LIGHTBOX_DELETE_WARTCHER": "Удалить наблюдателя..." + }, + "WATCH_BUTTON": { + "WATCH": "Отслеживать", + "WATCHING": "Отслеживаемое", + "UNWATCH": "Прекратить отслеживание", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Начать/прекратить отслеживание", + "COUNTER_TITLE": "{total, plural, one{один наблюдатель} other{# наблюдателя (-ей)}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Повысить", + "UPVOTED": "Повышено", + "DOWNVOTE": "Понизить", + "VOTERS": "Voters", + "BUTTON_TITLE": "Повысить/понизить", + "COUNTER_TITLE": "{total, plural, one{один голос} other{# голоса (-ов)}}" + }, + "CUSTOM_ATTRIBUTES": { + "CUSTOM_FIELDS": "Пользовательские поля", + "SAVE": "Сохранить поле", + "EDIT": "Редактировать поле", + "DELETE": "Удалить атрибут", + "CONFIRM_DELETE": "Не забудьте, что все значения для этого специального поля будут удалены.
Вы действительно хотите продолжить?" + }, + "FILTERS": { + "TITLE": "фильтры", + "INPUT_PLACEHOLDER": "Название ссылки", + "TITLE_ACTION_FILTER_BUTTON": "поиск", + "BREADCRUMB_TITLE": "назад к категориям", + "BREADCRUMB_FILTERS": "Фильтры", + "BREADCRUMB_STATUS": "cтатус" + }, + "WYSIWYG": { + "H1_BUTTON": "Заголовок первого уровня", + "H1_SAMPLE_TEXT": "Ваш заголовок здесь...", + "H2_BUTTON": "Заголовок второго уровня", + "H2_SAMPLE_TEXT": "Ваш заголовок здесь...", + "H3_BUTTON": "Заголовок третьего уровня", + "H3_SAMPLE_TEXT": "Ваш заголовок здесь...", + "BOLD_BUTTON": "Полужирный", + "BOLD_BUTTON_SAMPLE_TEXT": "Ваш текст здесь...", + "ITALIC_BUTTON": "Курсив", + "ITALIC_SAMPLE_TEXT": "Ваш текст здесь...", + "STRIKE_BUTTON": "Зачёркнутый", + "STRIKE_SAMPLE_TEXT": "Ваш текст здесь...", + "BULLETED_LIST_BUTTON": "Маркированный список", + "BULLETED_LIST_SAMPLE_TEXT": "Ваш текст здесь...", + "NUMERIC_LIST_BUTTON": "Нумерованный список", + "NUMERIC_LIST_SAMPLE_TEXT": "Ваш текст здесь...", + "PICTURE_BUTTON": "Изображение", + "PICTURE_SAMPLE_TEXT": "Альтернативный текст для изображения...", + "LINK_BUTTON": "Ссылка", + "LINK_SAMPLE_TEXT": "Ваш текст для ссылки здесь...", + "QUOTE_BLOCK_BUTTON": "Блок с цитатой", + "QUOTE_BLOCK_SAMPLE_TEXT": "Ваш текст здесь...", + "CODE_BLOCK_BUTTON": "Блок кода", + "CODE_BLOCK_SAMPLE_TEXT": "Ваш текст здесь...", + "PREVIEW_BUTTON": "Предварительный просмотр", + "EDIT_BUTTON": "Редактировать", + "MARKDOWN_HELP": "Помощь по синтаксису Markdown" + }, + "PERMISIONS_CATEGORIES": { + "SPRINTS": { + "NAME": "Спринты", + "VIEW_SPRINTS": "Посмотреть спринты", + "ADD_SPRINTS": "Добавить спринты", + "MODIFY_SPRINTS": "Изменить спринты", + "DELETE_SPRINTS": "Удалить спринты" + }, + "USER_STORIES": { + "NAME": "Истории от Пользователей", + "VIEW_USER_STORIES": "Посмотреть истории от пользователей", + "ADD_USER_STORIES": "Добавить истории от пользователей", + "MODIFY_USER_STORIES": "Изменить истории от пользователей", + "DELETE_USER_STORIES": "Удалить истории от пользователей" + }, + "TASKS": { + "NAME": "Задачи", + "VIEW_TASKS": "Просмотреть задачи", + "ADD_TASKS": "Добавить задачи", + "MODIFY_TASKS": "Редактировать задачи", + "DELETE_TASKS": "Удалить задачи" + }, + "ISSUES": { + "NAME": "Запросы", + "VIEW_ISSUES": "Посмотреть запросы", + "ADD_ISSUES": "Добавить запросы", + "MODIFY_ISSUES": "Изменить запросы", + "DELETE_ISSUES": "Удалить запросы" + }, + "WIKI": { + "NAME": "Вики", + "VIEW_WIKI_PAGES": "Посмотреть wiki-страницы", + "ADD_WIKI_PAGES": "Добавить wiki-страницы", + "MODIFY_WIKI_PAGES": "Модифицировать wiki-страницы", + "DELETE_WIKI_PAGES": "Удалить wiki-страницы", + "VIEW_WIKI_LINKS": "Посмотреть wiki-ссылки", + "ADD_WIKI_LINKS": "Добавить wiki-ссылки", + "DELETE_WIKI_LINKS": "Удалить wiki-ссылки" + } + }, + "META": { + "PAGE_TITLE": "Taiga", + "PAGE_DESCRIPTION": "Taiga - это платформа по управлению проектами для стартапов и agile-разработчиков/дизайнеров, которые хотят пользоваться простым и красивым инструментом, делающим их жизнь по-настоящему приятной." + } + }, + "LOGIN": { + "PAGE_TITLE": "Вход - Taiga", + "PAGE_DESCRIPTION": "Выполняется вход в Taiga, платформу для управления проектами стартапов, сделанную для разработчиков и дизайнеров, которые хотят простой и красивый инструмент превращающий работу в удовольствие." + }, + "AUTH": { + "INVITED_YOU": "пригласил Вас присоединиться к проекту", + "NOT_REGISTERED_YET": "Еще не зарегистрированы?", + "REGISTER": "Зарегистрироваться", + "CREATE_ACCOUNT": "Создать бесплатный аккаунт" + }, + "LOGIN_COMMON": { + "HEADER": "У меня уже есть логин в Taiga", + "PLACEHOLDER_AUTH_NAME": "Логин или email (с учетом регистра)", + "LINK_FORGOT_PASSWORD": "Забыли?", + "TITLE_LINK_FORGOT_PASSWORD": "Вы забыли пароль?", + "ACTION_ENTER": "Ввод", + "ACTION_SIGN_IN": "Войти", + "PLACEHOLDER_AUTH_PASSWORD": "Пароль (чувствителен к регистру)" + }, + "LOGIN_FORM": { + "ERROR_AUTH_INCORRECT": "Oompa Loompas считает, что Ваш логин, email или пароль неправильный.", + "SUCCESS": "Oompa Loompas счастлив, добро пожаловать в Тайгу!" + }, + "REGISTER": { + "PAGE_TITLE": "Зарегистрироваться", + "PAGE_DESCRIPTION": "Создайте аккаунт в Taiga — платформе для управления проектами стартапов, agile разработчиков и дизайнеров, которые хотят простой и красивый инструмент превращающий работу в удовольствие." + }, + "REGISTER_FORM": { + "TITLE": "Зарегистрируйте аккаунт Taiga (бесплатно)", + "PLACEHOLDER_NAME": "Выберите имя учётной записи (с учётом регистра)", + "PLACEHOLDER_FULL_NAME": "Введите Ваше полное имя", + "PLACEHOLDER_EMAIL": "Ваш email", + "PLACEHOLDER_PASSWORD": "Задайте новый пароль (с учетом регистра)", + "ACTION_SIGN_UP": "Зарегистрироваться", + "TITLE_LINK_LOGIN": "Войти", + "LINK_LOGIN": "Уже зарегистрированы? Авторизуйтесь" + }, + "FORGOT_PASSWORD": { + "PAGE_TITLE": "Забыли пароль?", + "PAGE_DESCRIPTION": "Введите Ваш логин или email для получения нового пароля и Вы сможете получить доступ к Taiga снова." + }, + "FORGOT_PASSWORD_FORM": { + "TITLE": "Упс, забыли пароль?", + "SUBTITLE": "Введите Ваш логин или email для получения нового пароля", + "PLACEHOLDER_FIELD": "Логин или e-mail", + "ACTION_RESET_PASSWORD": "Сбросить пароль", + "LINK_CANCEL": "Не, давай назад, думаю я вспомню.", + "SUCCESS": "Проверьте почту!
Мы выслали Вам письмо с инструкцией по установке нового пароля", + "ERROR": "Умпа-Лумпы говорят, что вы еще не зарегистрированы." + }, + "CHANGE_PASSWORD": { + "PAGE_TITLE": "Изменить пароль - Taiga", + "PAGE_DESCRIPTION": "Введите новый пароль для Вашего Taiga аккаунта и кстати, еда богатая железом — полезна для мозгов :P", + "SECTION_NAME": "Изменить пароль", + "FIELD_CURRENT_PASSWORD": "Текущий пароль", + "PLACEHOLDER_CURRENT_PASSWORD": "Ваш текущий пароль (или оставьте пустым, если пароль еще не задан)", + "FIELD_NEW_PASSWORD": "Новый пароль", + "PLACEHOLDER_NEW_PASSWORD": "Введите новый пароль", + "FIELD_RETYPE_PASSWORD": "Введите повторно новый пароль", + "PLACEHOLDER_RETYPE_PASSWORD": "Введите повторно новый пароль", + "ERROR_PASSWORD_MATCH": "Пароли не совпадают" + }, + "CHANGE_PASSWORD_RECOVERY_FORM": { + "TITLE": "Задать новый пароль", + "SUBTITLE": "И кстати, может быть Вам будет полезна еда богатая железом — она укрепляет память :P", + "PLACEHOLDER_NEW_PASSWORD": "Новый пароль", + "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Введите повторно новый пароль", + "ACTION_RESET_PASSWORD": "Сбросить пароль", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", + "SUCCESS": "Наши Умпа Лумпы сохранили ваш новый пароль.
Попробуйте войти, используя его." + }, + "INVITATION": { + "PAGE_TITLE": "Приглашение принято - Taiga", + "PAGE_DESCRIPTION": "Принять приглашение и присоединиться к Taiga — платформе для управления проектами стартапов, agile разработчиков и дизайнеров, которые хотят простой и красивый инструмент превращающий работу в удовольствие." + }, + "INVITATION_LOGIN_FORM": { + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", + "SUCCESS": "Вы успешно добавлены к проекту. Добро пожаловать в {{project_name}}", + "ERROR": "Умпа-Лумпы говорят, что вы не зарегистрированы или ввели неверный пароль." + }, + "HOME": { + "PAGE_TITLE": "Домашняя страница - Taiga", + "PAGE_DESCRIPTION": "Главная страница Тайги, с вашими основными проектами, назначенными и отслеживаемыми задачами, запросами и пользовательскими историями", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", + "EMPTY_PROJECT_LIST": "У Вас пока нет проектов", + "WORKING_ON_SECTION": "Работает над", + "WATCHING_SECTION": "Отслеживаемые" + }, + "PROJECTS": { + "PAGE_TITLE": "Мои проекты", + "PAGE_DESCRIPTION": "Список Ваших проектов, отсортируйте их или создайте новый.", + "MY_PROJECTS": "Мои проекты" + }, + "ATTACHMENT": { + "SECTION_NAME": "Вложения", + "TITLE": "{{ fileName }} загружен {{ date }}", + "DESCRIPTION": "Введите краткое описание", + "DEPRECATED": "(устаревший)", + "DEPRECATED_FILE": "Устаревший?", + "ADD": "Добавить вложение. {{maxFileSizeMsg}}", + "MAX_FILE_SIZE": "[Макс. размер: {{maxFileSize}}]", + "SHOW_DEPRECATED": "Показать устаревшие приложения", + "HIDE_DEPRECATED": "- спрятать устаревшие приложения", + "COUNT_DEPRECATED": "({{ counter }} устарело)", + "MAX_UPLOAD_SIZE": "Максимальный объем для загрузки {{maxFileSize}}", + "DATE": "DD MMM YYYY [в] hh:mm", + "ERROR_UPLOAD_ATTACHMENT": "Мы не смогли загрузить '{{fileName}}'. {{errorMessage}}", + "TITLE_LIGHTBOX_DELETE_ATTACHMENT": "Удалить вложение...", + "MSG_LIGHTBOX_DELETE_ATTACHMENT": "приложение '{{fileName}}'", + "ERROR_DELETE_ATTACHMENT": "Мы не смогли провести удаление: {{errorMessage}}", + "FIELDS": { + "IS_DEPRECATED": "рекомендовано" + } + }, + "PAGINATION": { + "PREVIOUS": "Пред.", + "NEXT": "Следующий" + }, + "ADMIN": { + "COMMON": { + "TITLE_ACTION_EDIT_VALUE": "Изменить значение", + "TITLE_ACTION_DELETE_VALUE": "Удалить значение" + }, + "HELP": "Вам нужна помощь? Проверьте нашу страницу техподдержки!", + "PROJECT_DEFAULT_VALUES": { + "TITLE": "Значения по умолчанию", + "SUBTITLE": "Задать начальные значения для всех полей" + }, + "MEMBERSHIPS": { + "TITLE": "Управление участниками", + "PAGE_TITLE": "Участие - {{projectName}}", + "ADD_BUTTON": "+ Новый участник", + "ADD_BUTTON_TITLE": "Добавить нового участника" + }, + "PROJECT_EXPORT": { + "TITLE": "Экспорт", + "SUBTITLE": "Экспортируйте ваш проект, чтобы сохранить резервную копию или создать новый, основываясь на текущем.", + "EXPORT_BUTTON": "Экспорт", + "EXPORT_BUTTON_TITLE": "Экспортировать проект", + "LOADING_TITLE": "Мы создали ваш файл резервной копии", + "DUMP_READY": "Файл резервной копии готов!", + "LOADING_MESSAGE": "Пожалуйста, не закрывайте эту страницу", + "ASYNC_MESSAGE": "Мы отправим вам email когда будет готово.", + "SYNC_MESSAGE": "Если загрузка не начинается самостоятельно, нажмите здесь.", + "ERROR": "У Oompa Loompas возникли проблемы при создании резервной копии. Повторите еще раз.", + "ERROR_BUSY": "Извините, Oompa Loompas очень загружен сейчас. Повторите через несколько минут.", + "ERROR_MESSAGE": "Умпа-Лумпы не смогли создать для вас дамп: {{message}}" + }, + "MODULES": { + "TITLE": "Модули", + "ENABLE": "Включить", + "DISABLE": "Выключить", + "BACKLOG": "Список задач", + "BACKLOG_DESCRIPTION": "Управляйте историями от пользователей, чтобы организовывать вашу ближайшую и приоритетную работу", + "KANBAN": "Kanban", + "KANBAN_DESCRIPTION": "Организуйте эффективную работу с проектом с помощью этой панели", + "ISSUES": "Запросы", + "ISSUES_DESCRIPTION": "Следите за ошибками, вопросами и улучшениями вашего проекта. Ничего не пропустите!", + "WIKI": "Вики", + "WIKI_DESCRIPTION": "Добавляйте, изменяйте или удаляйте контент совместно с остальными. Это самое правильное место для документации вашего проекта.", + "MEETUP": "Созвониться", + "MEETUP_DESCRIPTION": "Выберите вашу систему для видеоконференций. Даже разработчикам нужны митинги \"лицом к лицу\"", + "SELECT_VIDEOCONFERENCE": "Выберите систему видеоконференций", + "SALT_CHAT_ROOM": "Если хотите, можете добавить \"соль\" к имени чат-комнаты", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "Другое", + "URL_CHAT_ROOM": "Ссылка на Ваш чат" + }, + "PROJECT_PROFILE": { + "PAGE_TITLE": "{{sectionName}} - профиль Проекта - {{projectName}}", + "PROJECT_DETAILS": "Детали проекта", + "PROJECT_NAME": "Название проекта", + "PROJECT_SLUG": "Ссылочное имя проекта", + "NUMBER_SPRINTS": "Количество спринтов (0 для неопределенного количества)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", + "TAGS": "Тэги", + "DESCRIPTION": "Описание", + "PUBLIC_PROJECT": "Публичный проект", + "PRIVATE_PROJECT": "Закрытый проект", + "DELETE": "Удалить проект" + }, + "REPORTS": { + "TITLE": "Отчеты", + "SUBTITLE": "Экспортируйте данные вашего проекта в формате CSV и создавайте ваши собственные отчёты", + "DESCRIPTION": "Скачайте файл CSV или скопируйте сгенерированную ссылку, затем откройте в любом текстовом редакторе или редакторе таблиц, чтобы создать ваши собственные отчёты о проекте. Вы сможете легко визуализировать и анализировать ваши данные.", + "HELP": "Как использовать это в моей таблице?", + "REGENERATE_TITLE": "Изменить URL", + "REGENERATE_SUBTITLE": "Вы собираетесь изменить ссылку доступа к данным CSV. Прежний вариант ссылки перестанет работать. Вы уверены?" + }, + "CSV": { + "SECTION_TITLE_US": "Отчёты об историях от пользователей", + "SECTION_TITLE_TASK": "отчёты о задачах", + "SECTION_TITLE_ISSUE": "отчёты о запросах", + "DOWNLOAD": "Скачать CSV", + "URL_FIELD_PLACEHOLDER": "Упс, забыли пароль?", + "TITLE_REGENERATE_URL": " Сделать CSV ссылку ещё раз", + "ACTION_GENERATE_URL": "Сгенерировать ссылку", + "ACTION_REGENERATE": "Создать заново" + }, + "CUSTOM_FIELDS": { + "TITLE": "Пользовательские поля", + "SUBTITLE": "Укажите специальные поля для ваших пользовательских историй, задач и запросов", + "US_DESCRIPTION": "Специальные поля для историй от пользователей", + "US_ADD": "Добавить специальное поле для историй от пользователей", + "TASK_DESCRIPTION": "Специальные поля задач", + "TASK_ADD": "Добавить новые типы задач", + "ISSUE_DESCRIPTION": "Специальные поля запросов", + "ISSUE_ADD": "Добавить специальное поле для запросов", + "FIELD_TYPE_TEXT": "Текст", + "FIELD_TYPE_MULTI": "Многостроковое", + "FIELD_TYPE_DATE": "Дата" + }, + "PROJECT_VALUES": { + "PAGE_TITLE": "{{sectionName}} - Значения атрибутов проекта - {{projectName}}", + "REPLACEMENT": "Все объекты с этим значением будут заменены на", + "ERROR_DELETE_ALL": "Невозможно удалить все значения" + }, + "PROJECT_VALUES_POINTS": { + "TITLE": "Очки", + "SUBTITLE": "Введите приблизительное количество очков для пользовательских историй", + "US_TITLE": "Очки ПИ", + "ACTION_ADD": "Добавить очки" + }, + "PROJECT_VALUES_PRIORITIES": { + "TITLE": "Приоритеты", + "SUBTITLE": "Укажите, какие приоритеты будут иметь ваши запросы", + "ISSUE_TITLE": "Приоритеты запросов", + "ACTION_ADD": "Добавить новый приоритет" + }, + "PROJECT_VALUES_SEVERITIES": { + "TITLE": "Степени важности", + "SUBTITLE": "Укажите степени важности, которые будут иметь ваши запросы", + "ISSUE_TITLE": "Степени важности запросов", + "ACTION_ADD": "Добавить новую степень важности" + }, + "PROJECT_VALUES_STATUS": { + "TITLE": "Статус", + "SUBTITLE": "Укажите, какие статусы будут принимать ваши пользовательские истории, задачи и запросы", + "US_TITLE": "Статусы ПИ", + "TASK_TITLE": "Статус задач", + "ISSUE_TITLE": "Статусы запроса" + }, + "PROJECT_VALUES_TYPES": { + "TITLE": "Типы", + "SUBTITLE": "Укажите, какие типы смогут иметь ваши запросы", + "ISSUE_TITLE": "Типы запросов", + "ACTION_ADD": "Добавить новый" + }, + "ROLES": { + "PAGE_TITLE": "Роли - {{projectName}}", + "WARNING_NO_ROLE": "Осторожнее: ни с какими ролями на вашем проекте участники не смогут оценить очки для историй от пользователей.", + "HELP_ROLE_ENABLED": "Когда включено, участники, назначенные на эту роль, смогут оценивать очки для Историй от Пользователей", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", + "COUNT_MEMBERS": "{{ role.members_count }} участников с этой ролью", + "TITLE_DELETE_ROLE": "Удалить Роль", + "REPLACEMENT_ROLE": "Все пользователи этой роли будут перемещены в", + "WARNING_DELETE_ROLE": "Осторожнее, все оценки, связанные с этой будут удалены", + "ERROR_DELETE_ALL": "Вы не можете удалить все значения", + "EXTERNAL_USER": "Внешний пользователь" + }, + "THIRD_PARTIES": { + "SECRET_KEY": "Секретный ключ", + "PAYLOAD_URL": "Ссылка на полезную нагрузку", + "VALID_IPS": "Правильные IP-адреса (через ,)" + }, + "BITBUCKET": { + "SECTION_NAME": "Bitbucket", + "PAGE_TITLE": "Bitbucket - {{projectName}}", + "INFO_VERIFYING_IP": "Запросы к Bitbucket не подписаны, так что лучший способ их проверки - через IP-адрес. Если это поле пустое, то проверка по IP-адресу не будет проводиться." + }, + "GITLAB": { + "SECTION_NAME": "Gitlab", + "PAGE_TITLE": "Gitlab - {{projectName}}", + "INFO_VERIFYING_IP": "Запросы к Gitlab не подписаны, так что лучший способ их проверки - это проверка по IP-адресу. Если это поле пустое, IP-адрес не будет проверяться." + }, + "GITHUB": { + "SECTION_NAME": "Github", + "PAGE_TITLE": "Github - {{projectName}}" + }, + "WEBHOOKS": { + "PAGE_TITLE": "Web-зацепки - {{projectName}}", + "SECTION_NAME": "Web-зацепки", + "SUBTITLE": "Web-зацепки рассказывают внешним сервисам о событиях в Taiga, таких как комментарии, новые истории от пользователей и т.д.", + "ADD_NEW": "Добавить новую Web-зацепку", + "TYPE_NAME": "Укажите имя сервиса", + "TYPE_PAYLOAD_URL": "Укажите нагрузочную ссылку сервиса", + "TYPE_SERVICE_SECRET": "Введите секретный код сервиса", + "SAVE": "Сохранить Web-зацепку", + "CANCEL": "Отменить Web-зацепку", + "SHOW_HISTORY": "(Показать историю)", + "TEST": "Протестировать Web-зацепку", + "EDIT": "Редактировать Web-зацепку", + "DELETE": "Удалить Web-зацепку", + "REQUEST": "Запрос", + "RESEND_REQUEST": "Переслать запрос", + "HEADERS": "Заголовки", + "PAYLOAD": "Нагрузка", + "RESPONSE": "Ответ", + "DATE": "DD MMM YYYY [в] hh:mm:ss", + "ACTION_HIDE_HISTORY": "(Спрятать историю)", + "ACTION_HIDE_HISTORY_TITLE": "Скрыть детали истории", + "ACTION_SHOW_HISTORY": "(Показать историю)", + "ACTION_SHOW_HISTORY_TITLE": "Показать детали истории", + "WEBHOOK_NAME": "Web-зацепка '{{name}}'" + }, + "CUSTOM_ATTRIBUTES": { + "PAGE_TITLE": "{{sectionName}} - Специальные Атрибуты - {{projectName}}", + "ADD": "Добавить поле", + "EDIT": "Редактировать специальное поле", + "DELETE": "Удалить поле", + "SAVE_TITLE": "Сохранить поле", + "CANCEL_TITLE": "Отменить создание", + "SET_FIELD_NAME": "Введите имя поля", + "SET_FIELD_DESCRIPTION": "Назначьте описание специальному полю", + "ACTION_UPDATE": "Обновить поле", + "ACTION_CANCEL_EDITION": "Отменить правку" + }, + "MEMBERSHIP": { + "COLUMN_MEMBER": "Участник", + "COLUMN_ADMIN": "Админ", + "COLUMN_ROLE": "Роль", + "COLUMN_STATUS": "Статус", + "STATUS_ACTIVE": "Активный", + "STATUS_PENDING": "В ожидании", + "DELETE_MEMBER": "Удалить участника", + "RESEND": "Resend", + "SUCCESS_SEND_INVITATION": "Мы отправили ещё одно приглашение на адрес '{{email}}'.", + "ERROR_SEND_INVITATION": "Мы не выслали приглашение.", + "SUCCESS_DELETE": "Мы удалили {{message}}.", + "ERROR_DELETE": "Мы не смогли удалить {{message}}", + "DEFAULT_DELETE_MESSAGE": "приглашение на {{email}}" + }, + "DEFAULT_VALUES": { + "LABEL_POINTS": "Значения по умолчанию для выбора очков", + "LABEL_US": "Значение по умолчанию для статуса ПИ", + "LABEL_TASK_STATUS": "Значение по умолчанию для статуса задачи", + "LABEL_PRIORITY": "Значение по умолчанию для выбора приоритета", + "LABEL_SEVERITY": "Значение важности по умолчанию", + "LABEL_ISSUE_TYPE": "Значение по умолчанию для типа запроса", + "LABEL_ISSUE_STATUS": "Значение по умолчанию для статуса запроса" + }, + "STATUS": { + "PLACEHOLDER_WRITE_STATUS_NAME": "Укажите название для нового статуса" + }, + "TYPES": { + "PLACEHOLDER_WRITE_NAME": "Введите имя нового элемента" + }, + "US_STATUS": { + "ACTION_ADD_STATUS": "Добавить новый статус", + "IS_ARCHIVED_COLUMN": "В архиве?", + "WIP_LIMIT_COLUMN": "Лимит текущей работы", + "PLACEHOLDER_WRITE_NAME": "Укажите название для нового статуса" + }, + "MENU": { + "TITLE": "Админ", + "PROJECT": "Проект", + "ATTRIBUTES": "Атрибуты", + "MEMBERS": "Участники", + "PERMISSIONS": "Разрешения", + "INTEGRATIONS": "Интеграции", + "PLUGINS": "Плагины" + }, + "SUBMENU_PROJECT_ATTRIBUTES": { + "TITLE": "Атрибуты" + }, + "SUBMENU_PROJECT_VALUES": { + "STATUS": "Статус", + "POINTS": "Очки", + "PRIORITIES": "Приоритет", + "SEVERITIES": "Степени важности", + "TYPES": "Типы", + "CUSTOM_FIELDS": "Собственные поля" + }, + "SUBMENU_PROJECT_PROFILE": { + "TITLE": "Профиль проекта" + }, + "SUBMENU_ROLES": { + "TITLE": "Роли", + "ACTION_NEW_ROLE": "+ Новая роль", + "TITLE_ACTION_NEW_ROLE": "Добавить новую роль" + }, + "SUBMENU_THIDPARTIES": { + "TITLE": "Сервисы" + } + }, + "USER": { + "PROFILE": { + "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", + "EDIT": "Изменить профиль", + "FOLLOW": "Следить", + "CLOSED_US": "Закрытые ПИ", + "PROJECTS": "Проекты", + "PROJECTS_EMPTY": "{{username}} пока не имеет проектов", + "CONTACTS": "Контакты", + "CONTACTS_EMPTY": "{{username}} ещё не имеет контактов", + "CURRENT_USER_CONTACTS_EMPTY": "У вас пока нет контактов", + "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "Люди, с которыми вы работаете в Taiga, будут отмечены как ваши контакты автоматически", + "REPORT": "Пожаловаться", + "TABS": { + "ACTIVITY_TAB": "Действия", + "ACTIVITY_TAB_TITLE": "Показать все действия пользователя", + "PROJECTS_TAB": "Проекты", + "PROJECTS_TAB_TITLE": "Список всех проектов, в которых участвует пользователь", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "Голоса", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Просмотренные", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "Контакты", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } + }, + "PROFILE_SIDEBAR": { + "TITLE": "Ваш профиль", + "DESCRIPTION": "Люди могут видеть всё что Вы делаете и всё над чем Вы работаете. Вы также можете добавить краткую биографию.", + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Введите что-нибудь...", + "FILTER_TYPE_ALL": "Все", + "FILTER_TYPE_ALL_TITLE": "Показать все", + "FILTER_TYPE_PROJECTS": "Проекты", + "FILTER_TYPE_PROJECT_TITLES": "Показать только проекты", + "FILTER_TYPE_USER_STORIES": "Истории", + "FILTER_TYPE_USER_STORIES_TITLES": "Показывать только пользовательские истории", + "FILTER_TYPE_TASKS": "Задачи", + "FILTER_TYPE_TASK_TITLES": "Показывать только задачи", + "FILTER_TYPE_ISSUES": "Запросы", + "FILTER_TYPE_ISSUES_TITLE": "Показывать только запросы", + "EMPTY_TITLE": "It looks like there's nothing to show here." + } + }, + "PROJECT": { + "PAGE_TITLE": "{{projectName}}", + "WELCOME": "Добро пожаловать", + "SECTION_PROJECTS": "Проекты", + "HELP": "Реорганизуйте свои проекты так чтобы часто используемые были бы наверху.
Первые 10 проектов будут находится вверху списка проектов.", + "PRIVATE": "Закрытый проект", + "STATS": { + "PROJECT": "проекта
очки", + "DEFINED": "заданные
очки", + "ASSIGNED": "назначенные
очки", + "CLOSED": "закрытые
очки" + }, + "SECTION": { + "SEARCH": "Поиск", + "TIMELINE": "График работ", + "BACKLOG": "Список задач", + "KANBAN": "Kanban", + "ISSUES": "Запросы", + "WIKI": "Wiki", + "TEAM": "Команда", + "MEETUP": "Созвониться", + "ADMIN": "Админ" + }, + "NAVIGATION": { + "SECTION_TITLE": "Ваши проекты", + "PLACEHOLDER_SEARCH": "Искать в...", + "ACTION_CREATE_PROJECT": "Создать проект", + "ACTION_IMPORT_PROJECT": "Импортировать проект", + "SEE_MORE_PROJECTS": "Смотрите другие проекты", + "TITLE_CREATE_PROJECT": "Создать проект", + "TITLE_IMPORT_PROJECT": "Импортировать проект", + "TITLE_PRVIOUS_PROJECT": "Показать предыдущие проекты", + "TITLE_NEXT_PROJECT": "Показать следующие проекты", + "HELP_TITLE": "Страница поддержки Taiga", + "HELP": "Помощь", + "FEEDBACK_TITLE": "Оставить отзыв", + "FEEDBACK": "Обратная связь", + "NOTIFICATIONS_TITLE": "Настроить уведомления", + "NOTIFICATIONS": "Уведомления", + "ORGANIZATIONS_TITLE": "Редактировать Ваши организации", + "ORGANIZATIONS": "Настроить организации", + "SETTINGS_TITLE": "Редактировать настройки", + "SETTINGS": "Настройки", + "VIEW_PROFILE_TITLE": "Посмотреть профиль", + "VIEW_PROFILE": "Посмотреть профиль", + "EDIT_PROFILE_TITLE": "Править свой профиль", + "EDIT_PROFILE": "Править профиль", + "CHANGE_PASSWORD_TITLE": "Изменить пароль", + "CHANGE_PASSWORD": "Изменить пароль", + "DASHBOARD_TITLE": "Рабочий стол", + "DISCOVER_TITLE": "Найти проекты в тренде", + "DISCOVER": "Найти", + "ACTION_REORDER": "Можно перетаскивать мышкой" + }, + "IMPORT": { + "TITLE": "Импорт проекта", + "UPLOADING_FILE": "Загрузить свалочный файл", + "DESCRIPTION": "Этот процесс может занять некоторое время. Пожалуйста, сохраняйте это окно открытым.", + "ASYNC_IN_PROGRESS_TITLE": "Oompa Loompas импортирует ваш проект", + "ASYNC_IN_PROGRESS_MESSAGE": "Этот процесс может занять несколько минут
Мы отправим вам email, когда всё будет готово", + "UPLOAD_IN_PROGRESS_MESSAGE": "Загружено {{uploadedSize}} размера {{totalSize}}", + "ERROR": "У Oompa Loompas возникли проблемы при импорте резервной копии. Попробуйте еще раз.", + "ERROR_TOO_MANY_REQUEST": "Извините, Oompa Loompas очень загружен сейчас. Повторите через несколько минут.", + "ERROR_MESSAGE": "У Oompa Loompas возникли проблемы при импорте резервной копии: {{error_message}}", + "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) это слишком много для наших Умпа-Лумп, попробуйте еще раз с размером меньшим, чем ({{maxFileSize}})", + "SYNC_SUCCESS": "Импорт проекта выполнен успешно" + }, + "LIKE_BUTTON": { + "LIKE": "Мне нравится", + "LIKED": "Понравилось", + "UNLIKE": "Не нравится", + "BUTTON_TITLE": "Оценить проект", + "COUNTER_TITLE": "{total, plural, one{один фанат} other{# фаната (-ов)}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Отслеживать этот проект и настроить уведомления", + "WATCH": "Отслеживать", + "WATCHING": "Отслеживаемое", + "COUNTER_TITLE": "{total, plural, one{один наблюдатель} other{# наблюдателя (-ей)}}", + "OPTIONS": { + "NOTIFY_ALL": "Получать все уведомления", + "NOTIFY_ALL_TITLE": "Получать все уведомления для этого проекта", + "NOTIFY_INVOLVED": "Только вовлечённые", + "NOTIFY_INVOLVED_TITLE": "Получать уведомления только если вовлечён в работу", + "UNWATCH": "Прекратить отслеживание", + "UNWATCH_TITLE": "Прекратить отслеживание этого проекта" + } + } + }, + "LIGHTBOX": { + "DELETE_ACCOUNT": { + "SECTION_NAME": "Удалить аккаунт", + "CONFIRM": "Вы уверены, что хотите удалить ваш аккаунт?", + "SUBTITLE": "Мы будем скучать по вам! :-(", + "NEWSLETTER_LABEL_TEXT": "Я больше не хочу получать вашу новостную рассылку" + }, + "DELETE_PROJECT": { + "TITLE": "Удалить проект", + "QUESTION": "Вы уверены, что хотите удалить данный проект?", + "SUBTITLE": "Все данные проекта (пользовательские истории, задачи, запросы, спринты и вики страницы) будут утрачены! :-(", + "CONFIRM": "Да, я уверен" + }, + "ASSIGNED_TO": { + "SELECT": "Назначить", + "SEARCH": "Найти пользователей" + }, + "ADD_MEMBER": { + "TITLE": "Новый участник", + "HELP_TEXT": "Если пользователи уже зарегистрированы в Тайге они добавятся автоматически. В противном случае им будет отправлено приглашение." + }, + "CREATE_ISSUE": { + "TITLE": "Добавить запрос" + }, + "FEEDBACK": { + "TITLE": "Расскажи что-нибудь...", + "COMMENT": "... ошибки, предложения, что-нибудь клёвое... или даже Ваш самый страшный кошмар с Taiga", + "ACTION_SEND": "Оставить отзыв" + }, + "SEARCH": { + "TITLE": "Поиск", + "PLACEHOLDER_SEARCH": "Что вы ищете?" + }, + "ADD_EDIT_SPRINT": { + "TITLE": "Новый спринт", + "PLACEHOLDER_SPRINT_NAME": "Название спринта", + "PLACEHOLDER_SPRINT_START": "Начало - примерно", + "PLACEHOLDER_SPRINT_END": "Окончание - примерно", + "ACTION_DELETE_SPRINT": "Вы уверены, что хотите удалить данный спринт?", + "TITLE_ACTION_DELETE_SPRINT": "удалить спринт", + "LAST_SPRINT_NAME": "последний этап sprint - {{lastSprint}} ;-) " + }, + "CREATE_EDIT_TASK": { + "TITLE": "Новая задача", + "PLACEHOLDER_SUBJECT": "Название задачи", + "PLACEHOLDER_STATUS": "Статус задачи", + "OPTION_UNASSIGNED": "Не назначено", + "PLACEHOLDER_SHORT_DESCRIPTION": "Введите краткое описание", + "ACTION_EDIT": "Редактировать задачу" + }, + "CREATE_EDIT_US": { + "TITLE": "Новая ПИ", + "PLACEHOLDER_DESCRIPTION": "Пожалуйста, добавьте описание, чтобы другие лучше поняли эту ПИ", + "NEW_US": "Новая пользовательская история", + "EDIT_US": "Изменить пользовательскую историю" + }, + "DELETE_SPRINT": { + "TITLE": "Удалить спринт" + }, + "CREATE_MEMBER": { + "PLACEHOLDER_INVITATION_TEXT": "(Необязательно) Добавьте персональный текст в приглашение. Скажите что-нибудь приятное вашим новым участникам ;-)", + "PLACEHOLDER_TYPE_EMAIL": "Укажите e-mail" + } + }, + "US": { + "PAGE_TITLE": "{{userStorySubject}} - История от Пользователя {{userStoryRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Статус: {{userStoryStatus }}. Выполнено {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} из {{userStoryTotalTasks}} задач). Очки: {{userStoryPoints}}. Описание: {{userStoryDescription}}", + "SECTION_NAME": "Детали пользовательских историй", + "LINK_TASKBOARD": "Панель задач", + "TITLE_LINK_TASKBOARD": "Перейти к панели задач", + "TOTAL_POINTS": "общее число очков", + "ADD": "+ Добавить Пользовательскую Историю", + "ADD_BULK": "Добавить несколько Историй от Пользователей пакетно", + "PROMOTED": "Эта ПИ была сформирована из запроса:", + "TITLE_LINK_GO_TO_ISSUE": "Перейти к запросу", + "EXTERNAL_REFERENCE": "Эта ПИ была создана из:", + "GO_TO_EXTERNAL_REFERENCE": "Перейти в начало", + "BLOCKED": "Эта пользовательская история заблокирована ", + "PREVIOUS": "предыдущая пользовательская история", + "NEXT": "следующая пользовательская история", + "TITLE_DELETE_ACTION": "Удалить пользовательскую историю", + "LIGHTBOX_TITLE_BLOKING_US": "Блокирующая ПИ", + "TASK_COMPLETED": "{{totalClosedTasks}}/{{totalTasks}} задач выполнено", + "ASSIGN": "Поручить пользовательскую историю", + "NOT_ESTIMATED": "Не оценено", + "TOTAL_US_POINTS": "Всего очков за ПИ", + "FIELDS": { + "TEAM_REQUIREMENT": "Требование от Команды", + "CLIENT_REQUIREMENT": "Требование клиента", + "FINISH_DATE": "Дата завершения" + } + }, + "COMMENTS": { + "DELETED_INFO": "Комментарий удален {{user}} {{date}}", + "TITLE": "Комментарии", + "COMMENT": "Комментарий", + "TYPE_NEW_COMMENT": "Добавить комментарий", + "SHOW_DELETED": "Показать удаленный комментарий", + "HIDE_DELETED": "Скрыть удаленный комментарий", + "RESTORE": "Показать удаленный комментарий" + }, + "ACTIVITY": { + "SHOW_ACTIVITY": "Показать действия", + "DATETIME": "DD MMM YYYY HH:mm", + "SHOW_MORE": "+ Показать предыдущие записи (ещё {{showMore}})", + "TITLE": "Действия", + "REMOVED": "удален", + "ADDED": "добавлено", + "US_POINTS": "ПИ очки ({{name}})", + "NEW_ATTACHMENT": "новое вложение", + "DELETED_ATTACHMENT": "удаленное вложение", + "UPDATED_ATTACHMENT": "обновлено приложение {{filename}}", + "DELETED_CUSTOM_ATTRIBUTE": "удалить атрибут", + "SIZE_CHANGE": "Сделано {size, plural, one{изменение} other{# изменений}}", + "VALUES": { + "YES": "да", + "NO": "нет", + "EMPTY": "пусто", + "UNASSIGNED": "нет ответственного" + }, + "FIELDS": { + "SUBJECT": "название", + "NAME": "имя", + "DESCRIPTION": "описание", + "CONTENT": "контент", + "STATUS": "cтатус", + "IS_CLOSED": "закрыто", + "FINISH_DATE": "дата окончания", + "TYPE": "тип", + "PRIORITY": "приоритет", + "SEVERITY": "важность", + "ASSIGNED_TO": "назначено на", + "WATCHERS": "наблюдатели", + "MILESTONE": "спринт", + "USER_STORY": "пользовательская история", + "PROJECT": "проект", + "IS_BLOCKED": "заблокирован", + "BLOCKED_NOTE": "Пояснение блокировки", + "POINTS": "очков", + "CLIENT_REQUIREMENT": "требование клиента", + "TEAM_REQUIREMENT": "требование команды", + "IS_IOCAINE": "- иокаин", + "TAGS": "тэги", + "ATTACHMENTS": "Вложения", + "IS_DEPRECATED": "рекомендовано", + "ORDER": "порядок", + "BACKLOG_ORDER": "порядок списка задач", + "SPRINT_ORDER": "порядок спринтов", + "KANBAN_ORDER": "порядок kanban", + "TASKBOARD_ORDER": "порядок панели задач", + "US_ORDER": "порядок ПИ" + } + }, + "BACKLOG": { + "PAGE_TITLE": "Список задач - {{projectName}}", + "PAGE_DESCRIPTION": "Список задач с историями от пользователей и спринтами проекта {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Список задач", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "Админ", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", + "MOVE_US_TO_CURRENT_SPRINT": "Перейти к текущему спринту", + "SHOW_FILTERS": "Показать фильтры", + "SHOW_TAGS": "Показать теги", + "EMPTY": "Ваш список задач пуст!", + "CREATE_NEW_US": "Создать новую ПИ", + "CREATE_NEW_US_EMPTY_HELP": "Возможно будет лучше создать пользовательскую историю?", + "EXCESS_OF_POINTS": "Избыток очков", + "PENDING_POINTS": "Текущие очки", + "CLOSED_POINTS": "закрыта", + "COMPACT_SPRINT": "Компактный Спринт", + "GO_TO_TASKBOARD": "Перейти на панель задач: {{::name}}", + "EDIT_SPRINT": "Изменить Спринт", + "TOTAL_POINTS": "всего", + "STATUS_NAME": "Статус", + "SORTABLE_FILTER_ERROR": "Вы не можете перетаскивать на список задач, когда фильтры открыты", + "DOOMLINE": "Рамки Проекта [Крайний срок]", + "CHART": { + "XAXIS_LABEL": "Спринты", + "YAXIS_LABEL": "Очки", + "OPTIMAL": "Оптимальное число очков для работы по спринту \"{{sprintName}}\" должно быть {{value}}", + "REAL": "Реальные очки для работы по спринту \"{{sprintName}}\" - {{value}}", + "INCREMENT_TEAM": "Для спринта \"{{sprintName}}\" очки увеличены на {{value}} засчёт требований от команды", + "INCREMENT_CLIENT": "Для спринта \"{{sprintName}}\" очки увеличены на {{value}} засчёт требований от клиентов" + }, + "TAGS": { + "TOGGLE": "Переключить видимость тэга", + "SHOW": "Показать теги", + "HIDE": "Скрыть тэги" + }, + "TABLE": { + "COLUMN_US": "Истории от Пользователей", + "TITLE_COLUMN_POINTS": "Выберите вид для Роли" + }, + "SPRINT_SUMMARY": { + "TOTAL_POINTS": "всего
очков", + "COMPLETED_POINTS": "получено
очков", + "OPEN_TASKS": "открыть
задачи", + "CLOSED_TASKS": "завершённые
задачи", + "IOCAINE_DOSES": "иокаина
дозы", + "SHOW_STATISTICS_TITLE": "Показать статистику", + "TOGGLE_BAKLOG_GRAPH": "Показать/Скрыть график решения задач" + }, + "SUMMARY": { + "PROJECT_POINTS": "проектные
очки", + "DEFINED_POINTS": "определено
очков", + "CLOSED_POINTS": "закрытые
очки", + "POINTS_PER_SPRINT": "очков на /
спринт" + }, + "FILTERS": { + "TOGGLE": "Переключить видимость фильтров", + "TITLE": "Фильтры", + "REMOVE": "Сбросить фильтры", + "HIDE": "Спрятать фильтры", + "SHOW": "Показать фильтры", + "FILTER_CATEGORY_STATUS": "Статус", + "FILTER_CATEGORY_TAGS": "Тэги" + }, + "SPRINTS": { + "TITLE": "СПРИНТЫ", + "DATE": "DD MMM YYYY", + "LINK_TASKBOARD": "Панель задач Спринта", + "TITLE_LINK_TASKBOARD": "Перейти к Панели Задач \"{{name}}\"", + "NUMBER_SPRINTS": "
спринты", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Добавить новый спринт", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", + "ACTION_SHOW_CLOSED_SPRINTS": "Показать закрытые спринты", + "ACTION_HIDE_CLOSED_SPRINTS": "Спрятать закрытые спринты" + } + }, + "ERROR": { + "TEXT1": "Что-то случилось, Oompa Loompas работает над этим.", + "NOT_FOUND": "Не найдено", + "NOT_FOUND_TEXT": "Ошибка 404. Страница не найдена.", + "PERMISSION_DENIED": "Доступ закрыт", + "PERMISSION_DENIED_TEXT": "У вас нет прав для доступа к этой странице.", + "VERSION_ERROR": "Кто-то в Тайге изменил это раньше вас и Умпа-Лумпы не могут применить ваши изменения. Пожалуйста обновите страницу (Внимание! При обновлении страницы текущие правки будут утеряны) и внесите правки еще раз." + }, + "TASKBOARD": { + "PAGE_TITLE": "{{sprintName}} - панель задач спринта - {{projectName}}", + "PAGE_DESCRIPTION": "Спринт {{sprintName}} (с {{startDate}} по {{endDate}}) проекта {{projectName}}. Выполнено {{completedPercentage}}% ({{completedPoints}} из {{totalPoints}} очков). Открытые задачи: {{openTasks}} из {{totalTasks}}.", + "SECTION_NAME": "Панель задач", + "TITLE_ACTION_ADD": "Добавить новую Задачу", + "TITLE_ACTION_ADD_BULK": "Добавить новые задачи пакетно", + "TITLE_ACTION_ASSIGN": "Назначить задачу", + "TITLE_ACTION_EDIT": "Редактировать задачу", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", + "TABLE": { + "COLUMN": "Пользовательская история", + "TITLE_ACTION_FOLD": "Свернуть колонку", + "TITLE_ACTION_UNFOLD": "Развернуть колонку", + "TITLE_ACTION_FOLD_ROW": "Свернуть ряд", + "TITLE_ACTION_UNFOLD_ROW": "Развернуть ряд", + "FIELD_POINTS": "очков", + "ROW_UNASSIGED_TASKS_TITLE": "Не назначенные задачи" + }, + "CHARTS": { + "XAXIS_LABEL": "Дней", + "YAXIS_LABEL": "Очки", + "OPTIMAL": "Оптимальное количество очков для работы на день {{formattedDate}}: {{roundedValue}}", + "REAL": "Реальные очки для работы на день: {{formattedDate}} - {{roundedValue}}", + "DATE": "DD MMMM YYYY" + } + }, + "TASK": { + "PAGE_TITLE": "{{taskSubject}} - Задача {{taskRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Статус: {{taskStatus }}. Описание: {{taskDescription}}", + "SECTION_NAME": "Детали задачи", + "LINK_TASKBOARD": "Панель задач", + "TITLE_LINK_TASKBOARD": "Перейти к панели задач", + "PLACEHOLDER_SUBJECT": "Укажите новое название задачи", + "TITLE_SELECT_STATUS": "Статус", + "OWNER_US": "Эта задача принадлежит:", + "TITLE_LINK_GO_OWNER": "Перейти к пользовательской истории", + "ORIGIN_US": "Эта задача была создана из", + "TITLE_LINK_GO_ORIGIN": "Перейти к пользовательской истории", + "BLOCKED": "Эта задача заблокирована", + "PREVIOUS": "предыдущая задача", + "NEXT": "следующая задача", + "TITLE_DELETE_ACTION": "Удалить задачу", + "LIGHTBOX_TITLE_BLOKING_TASK": "Блокирующее задание", + "FIELDS": { + "MILESTONE": "Спринт", + "USER_STORY": "Пользовательская история", + "IS_IOCAINE": "- иокаин" + }, + "ACTION_IOCAINE": "иокаин", + "TITLE_ACTION_IOCAINE": "Чувствуете, что задание берет верх над вами? Дайте другим знать об этом, нажав на \"Иокаин\", когда редактируете задание. Возможно стать неуязвимым к этому (выдуманному) смертельном яду, потребляя небольшие количества время от времени, так же как возможно стать лучше в том, что вы делаете, временами беря на себя дополнительные препятствия!" + }, + "NOTIFICATION": { + "OK": "Всё хорошо", + "WARNING": "Упс, что-то случилось...", + "WARNING_TEXT": "Oompa Loompas сказал, что ваши изменения не сохранились!", + "SAVED": "Oompa Loompas сохранил изменения!", + "CLOSE": "закрыть уведомление", + "MAIL": "Уведомления почтой", + "ASK_DELETE": "Вы уверены, что хотите удалить?" + }, + "CANCEL_ACCOUNT": { + "TITLE": "Аннулировать ваш аккаунт", + "SUBTITLE": "Жаль, что вы покидаете taiga, надеемся, вам здесь понравилось :)", + "PLACEHOLDER_INPUT_TOKEN": "идентификатор аннулирования аккаунта", + "ACTION_LEAVING": "Да, я ухожу!", + "SUCCESS": "Oompa Loompas удалил ваш аккаунт" + }, + "CHANGE_EMAIL_FORM": { + "TITLE": "Изменить e-mail", + "SUBTITLE": "Ещё один клик и Ваш email будет обновлён!", + "PLACEHOLDER_INPUT_TOKEN": "изменить идентификатор email", + "ACTION_CHANGE_EMAIL": "изменить почту", + "SUCCESS": "Oompa Loompas обновил ваш e-mail" + }, + "ISSUES": { + "PAGE_TITLE": "Запросы - {{projectName}}", + "PAGE_DESCRIPTION": "Панель запросов проекта {{projectName}}: {{projectDescription}}", + "LIST_SECTION_NAME": "Запросы", + "SECTION_NAME": "Детали запроса", + "ACTION_NEW_ISSUE": "+НОВЫЙ ЗАПРОС", + "ACTION_PROMOTE_TO_US": "Повысить до пользовательской истории", + "PLACEHOLDER_FILTER_NAME": "Введите название фильтра и нажмите \"ввод\"", + "PROMOTED": "Этот запрос был переделан в ПИ:", + "EXTERNAL_REFERENCE": "Этот запрос был создан из", + "GO_TO_EXTERNAL_REFERENCE": "Перейти в начало", + "BLOCKED": "Этот запрос заблокирована", + "TITLE_PREVIOUS_ISSUE": "предыдущий запрос", + "TITLE_NEXT_ISSUE": "следующий запрос", + "ACTION_DELETE": "Удалить запрос", + "LIGHTBOX_TITLE_BLOKING_ISSUE": "Блокирующий запрос", + "FIELDS": { + "PRIORITY": "Приоритет", + "SEVERITY": "Важность", + "TYPE": "Тип" + }, + "CONFIRM_PROMOTE": { + "TITLE": "Превратить этот запрос в новую пользовательскую историю", + "MESSAGE": "Вы уверены, что хотите создать новую ПИ из этого запроса?" + }, + "FILTERS": { + "TITLE": "Фильтры", + "INPUT_SEARCH_PLACEHOLDER": "Название ссылки", + "TITLE_ACTION_SEARCH": "Поиск", + "ACTION_SAVE_CUSTOM_FILTER": "сохранить как специальный фильтр", + "BREADCRUMB": "Фильтры", + "TITLE_BREADCRUMB": "Фильтры", + "CATEGORIES": { + "TYPE": "Тип", + "STATUS": "Статус", + "SEVERITY": "Важность", + "PRIORITIES": "Приоритет", + "TAGS": "Тэги", + "ASSIGNED_TO": "Назначено", + "CREATED_BY": "Создано", + "CUSTOM_FILTERS": "Собственные фильтры" + }, + "CONFIRM_DELETE": { + "TITLE": "Удалить фильтр", + "MESSAGE": "специальный фильтр '{{customFilterName}}'" + } + }, + "TABLE": { + "COLUMNS": { + "TYPE": "Тип", + "SEVERITY": "Важность", + "PRIORITY": "Приоритет", + "SUBJECT": "Тема", + "VOTES": "Голоса", + "STATUS": "Статус", + "CREATED": "Создан", + "ASSIGNED_TO": "Назначено" + }, + "TITLE_ACTION_CHANGE_STATUS": "Изменить статус", + "TITLE_ACTION_ASSIGNED_TO": "Назначено", + "BLOCKED": "Заблокирован", + "EMPTY": { + "TITLE": "Нет присланных запросов :-)", + "SUBTITLE": "Вы нашли запрос?" + } + } + }, + "ISSUE": { + "PAGE_TITLE": "{{issueSubject}} - Запрос {{issueRef}} - {{projectName}}", + "PAGE_DESCRIPTION": "Статус: {{issueStatus }}. Тип: {{issueType}}, Приоритет: {{issuePriority}}. Важность: {{issueSeverity}}. Описание: {{issueDescription}}" + }, + "KANBAN": { + "PAGE_TITLE": "Kanban - {{projectName}}", + "PAGE_DESCRIPTION": "Панель kanban, с Историями от Пользователей для проекта {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Kanban", + "TITLE_ACTION_FOLD": "Свернуть колонку", + "TITLE_ACTION_UNFOLD": "Развернуть колонку", + "TITLE_ACTION_FOLD_CARDS": "Свернуть карточки", + "TITLE_ACTION_UNFOLD_CARDS": "Развернуть карточки", + "TITLE_ACTION_ADD_US": "Добавить Пользовательскую Историю", + "TITLE_ACTION_ADD_BULK": "Добавить пакетно", + "ACTION_SHOW_ARCHIVED": "Показать архивные", + "ACTION_HIDE_ARCHIVED": "Спрятать архив", + "HIDDEN_USER_STORIES": "Истории от Пользователей в этом статусе скрыты по умолчанию", + "ARCHIVED": "Вы заархивировали", + "UNDO_ARCHIVED": "Для отмены надо перетащить и сбросить ещё раз", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" + }, + "SEARCH": { + "PAGE_TITLE": "Поиск - {{projectName}}", + "PAGE_DESCRIPTION": "Ищите что угодно, пользовательские истории, задачи, запросы и вики-страницы, в проекте {{projectName}}: {{projectDescription}}", + "FILTER_USER_STORIES": "Истории от Пользователей", + "FILTER_ISSUES": "Запросы", + "FILTER_TASKS": "Задачи", + "FILTER_WIKI": "Вики страницы", + "PLACEHOLDER_SEARCH": "Искать в...", + "TITLE_ACTION_SEARCH": "поиск", + "EMPTY_TITLE": "Кажется, по вашему запросу ничего не найдено.", + "EMPTY_DESCRIPTION": "Попробуйте одну из вкладок выше и запустите поиск ещё раз" + }, + "TEAM": { + "PAGE_TITLE": "Команда - {{projectName}}", + "PAGE_DESCRIPTION": "Панель команды показывает всех участников проекта {{projectName}}: {{projectDescription}}", + "SECTION_NAME": "Команда", + "APP_TITLE": "КОМАНДА - {{projectName}}", + "PLACEHOLDER_INPUT_SEARCH": "Искать по имени", + "COLUMN_MR_WOLF": "Мр. Вульф", + "EXPLANATION_COLUMN_MR_WOLF": "Закрытые запросы", + "COLUMN_IOCAINE": "Употребляющий иокаин", + "EXPLANATION_COLUMN_IOCAINE": "потреблено доз иокаина", + "COLUMN_CERVANTES": "Сервантес", + "EXPLANATION_COLUMN_CERVANTES": "Отредактировано вики-страниц", + "COLUMN_BUG_HUNTER": "Охотник за ошибками", + "EXPLANATION_COLUMN_BUG_HUNTER": "Присланные запросы", + "COLUMN_NIGHT_SHIFT": "Ночная смена", + "EXPLANATION_COLUMN_NIGHT_SHIFT": "Задача закрыта", + "COLUMN_TOTAL_POWER": "Могущество", + "EXPLANATION_COLUMN_TOTAL_POWER": "Всего очков", + "SECTION_TITLE_TEAM": "Команда", + "SECTION_FILTER_ALL": "Все", + "CONFIRM_LEAVE_PROJECT": "Вы уверены, что хотите покинуть данный проект?", + "ACTION_LEAVE_PROJECT": "Покинуть проект" + }, + "USER_SETTINGS": { + "AVATAR_MAX_SIZE": "[макс. размер: {{maxFileSize}}]", + "MENU": { + "SECTION_TITLE": "Настройки пользователя", + "USER_PROFILE": "Профиль пользователя", + "CHANGE_PASSWORD": "Изменить пароль", + "EMAIL_NOTIFICATIONS": "Уведомления почтой" + }, + "NOTIFICATIONS": { + "SECTION_NAME": "Уведомления почтой", + "COLUMN_PROJECT": "Проект", + "COLUMN_RECEIVE_ALL": "Получать Всё", + "COLUMN_ONLY_INVOLVED": "Только задействованные", + "COLUMN_NO_NOTIFICATIONS": "Уведомлений нет", + "OPTION_ALL": "Все", + "OPTION_INVOLVED": "Вовлечен", + "OPTION_NONE": "Нет" + }, + "POPOVER": { + "USER_PROFILE": "Профиль пользователя", + "CHANGE_PASSWORD": "Изменить пароль", + "NOTIFICATIONS": "Уведомления", + "FEEDBACK": "Обратная связь", + "TITLE_AVATAR": "Настройки пользователя" + } + }, + "USER_PROFILE": { + "IMAGE_HELP": "Изображение будет отмасштабировано до 80x80px.
", + "ACTION_CHANGE_IMAGE": "Изменить", + "ACTION_USE_GRAVATAR": "Испольвать аватар из gravatar", + "ACTION_DELETE_ACCOUNT": "Удалить аккаунт", + "CHANGE_EMAIL_SUCCESS": "Проверьте входящие письма!
Мы послали письмо на ваш аккаунт
с инструкциями по установке вашего нового адреса.", + "CHANGE_PHOTO": "Изменить фото", + "FIELD": { + "USERNAME": "Имя пользователя", + "EMAIL": "Email", + "FULL_NAME": "Полное имя", + "PLACEHOLDER_FULL_NAME": "Установите ваше полное имя (например, Игорь Николаев)", + "BIO": "Биография (не более 210 символов)", + "PLACEHOLDER_BIO": "Расскажите нам что-нибудь о себе", + "LANGUAGE": "Язык", + "LANGUAGE_DEFAULT": "-- использовать язык по умолчанию --", + "THEME": "Тема", + "THEME_DEFAULT": "-- использовать тему по умолчанию --" + } + }, + "WIZARD": { + "SECTION_TITLE_CHOOSE_TEMPLATE": "Выберете шаблон", + "CHOOSE_TEMPLATE_TEXT": "Какой шаблон лучше всего подойдет для Вашего проекта?", + "SECTION_TITLE_CREATE_PROJECT": "Создать проект", + "CREATE_PROJECT_TEXT": "Свежий и чистый! Так здóрово!", + "PROGRESS_TEMPLATE_SELECTION": "Выбор шаблона", + "PROGRESS_NAME_DESCRIPTION": "Название и описание" + }, + "WIKI": { + "PAGE_TITLE": "{{wikiPageName}} - Вики - {{projectName}}", + "PAGE_DESCRIPTION": "Последнее редактирование: {{lastModifiedDate}} ({{totalEditions}} - всего правок) Контент: {{ wikiPageContent }}", + "DATETIME": "DD MMM YYYY HH:mm", + "PLACEHOLDER_PAGE": "Создать вики страницу", + "REMOVE": "Удалить эту вики страницу", + "DELETE_LIGHTBOX_TITLE": "Удалить вики страницу", + "NAVIGATION": { + "SECTION_NAME": "Ссылки", + "ACTION_ADD_LINK": "Добавить ссылку" + }, + "SUMMARY": { + "TIMES_EDITED": "раз
отредактировано", + "LAST_EDIT": "последняя
правка", + "LAST_MODIFICATION": "последнее изменение" + } + }, + "HINTS": { + "SECTION_NAME": "Подсказка", + "LINK": "Чтобы узать как пользовать этим — посетите нашу страницу поддержки.", + "LINK_TITLE": "Посетить нашу страницу поддержки", + "HINT1_TITLE": "Вы знали что можно импортировать и экспортировать проекты?", + "HINT1_TEXT": "Это позволяет вам собирать данные из одной Taiga и копировать в другую.", + "HINT2_TITLE": "Вы знали что можно создавать собственные поля?", + "HINT2_TEXT": "Команды теперь могут создавать специальные поля в качестве гибкого средства для ввода специфических данных их конкретного процесса работы", + "HINT3_TITLE": "Отсортируйте ваши проекты по признаку, наиболее важному для вас", + "HINT3_TEXT": "Главные 10 проектов будут доступны непосредственно из верхней панели", + "HINT4_TITLE": "Вы забыли над чем работали?", + "HINT4_TEXT": "Не переживайте, на вашем рабочем столе вы найдёте ваши активные задачи, запросы и пользовательские истории в том порядке, в котором вы над ними работали." + }, + "TIMELINE": { + "UPLOAD_ATTACHMENT": "{{username}} приложил новый файл к {{obj_name}}", + "US_CREATED": "{{username}} создал новую ПИ {{obj_name}} в {{project_name}}", + "ISSUE_CREATED": "{{username}} создал новый запрос {{obj_name}} в {{project_name}}", + "TASK_CREATED": "{{username}} создал новую задачу {{obj_name}} в {{project_name}}", + "TASK_CREATED_WITH_US": "{{username}} создал новую задачу {{obj_name}} в {{project_name}}, которая принадлежит Задаче от Пользователя {{us_name}}", + "WIKI_CREATED": "{{username}} создал новую вики-страницу {{obj_name}} в {{project_name}}", + "MILESTONE_CREATED": "{{username}} создал новый спринт {{obj_name}} в {{project_name}}", + "NEW_PROJECT": "{{username}} создал проект {{project_name}}", + "MILESTONE_UPDATED": "{{username}} обновил спринт {{obj_name}}", + "US_UPDATED": "{{username}} обновил атрибут \"{{field_name}}\" ПИ {{obj_name}}", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} установил атрибут \"{{field_name}}\" для ПИ {{obj_name}} на {{new_value}}", + "US_UPDATED_POINTS": "{{username}} установил очки для '{{role_name}}' для ПИ {{obj_name}} на {{new_value}}", + "ISSUE_UPDATED": "{{username}} обновил атрибут \"{{field_name}}\" запроса {{obj_name}}", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} установил атрибут \"{{field_name}}\" запроса {{obj_name}} на {{new_value}}", + "TASK_UPDATED": "{{username}} установил атрибут \"{{field_name}}\" задачи {{obj_name}} на {{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} установил атрибут \"{{field_name}}\" задачи {{obj_name}} на {{new_value}}", + "TASK_UPDATED_WITH_US": "{{username}} изменил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} установил атрибут \"{{field_name}}\" задачи {{obj_name}}, которая принадлежит ПИ {{us_name}}, на {{new_value}}", + "WIKI_UPDATED": "{{username}} обновил вики-страницу {{obj_name}}", + "NEW_COMMENT_US": "{{username}} прокомментировал ПИ {{obj_name}}", + "NEW_COMMENT_ISSUE": "{{username}} прокомментировал запрос {{obj_name}}", + "NEW_COMMENT_TASK": "{{username}} прокомментировал задачу {{obj_name}}", + "NEW_MEMBER": "У {{project_name}} появился новый участник", + "US_ADDED_MILESTONE": "{{username}} добавил ПИ {{obj_name}} для {{sprint_name}}", + "US_MOVED": "{{username}} переместил ПИ {{obj_name}}", + "US_REMOVED_FROM_MILESTONE": "{{username}} добавил ПИ {{obj_name}} к списку задач", + "BLOCKED": "{{username}} заблокировал {{obj_name}}", + "UNBLOCKED": "{{username}} разблокировал {{obj_name}}", + "NEW_USER": "{{username}} присоединился к Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "Внешнее приложение требующие аутентификации", + "PAGE_DESCRIPTION": "Внешнее приложение требующие аутентификации", + "AUTHORIZATION_REQUEST": "Разрешить {{application}} использовать ваш Taiga аккаунт?", + "LOGIN_WITH_ANOTHER_USER": "Войти под другим пользователем", + "AUTHORIZE_APP": "Авторизировать приложение", + "CANCEL": "Отмена" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "Ваш проект", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "Работает над", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "Отслеживаемое", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Удачи!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Содержание проекта", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "Спринты", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "Истории от Пользователей", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Удачи!" + } + } + } +} \ No newline at end of file diff --git a/app/locales/locale-zh-hant.json b/app/locales/locale-zh-hant.json index c4d753da..1f22a88f 100644 --- a/app/locales/locale-zh-hant.json +++ b/app/locales/locale-zh-hant.json @@ -36,6 +36,7 @@ "EXTERNAL_USER": "外部使用者", "GENERIC_ERROR": "我們的系統指出{{error}}.", "IOCAINE_TEXT": "感到任務的不堪負荷?確認其它人在編輯任務時知道此狀況,可以點選Iocaine。它可能會成為免疫的致命狀況,只要長期小量消耗,但如果你只是偶而挑戰它可有助表現。", + "CAPSLOCK_WARNING": "Be careful! You're writing with capital letters and this input is case sensitive.", "FORM_ERRORS": { "DEFAULT_MESSAGE": "該數值似乎為無效", "TYPE_EMAIL": "該電子郵件應為有效地址", @@ -130,7 +131,8 @@ "POINTS": "點數", "BLOCKED_NOTE": "已封鎖之筆記", "IS_BLOCKED": "封鎖", - "REF": "Ref" + "REF": "Ref", + "VOTES": "投票數" }, "ROLES": { "ALL": "所有" @@ -148,11 +150,27 @@ "OPEN": "開啟" }, "WATCHERS": { - "ADD": "新增監督者", - "TITLE": "監督者", + "ADD": "Add watchers", + "TITLE_ADD": "Add a project member to the watchers list", "DELETE": "已刪除監督者", "TITLE_LIGHTBOX_DELETE_WARTCHER": "刪除監督者" }, + "WATCH_BUTTON": { + "WATCH": "Watch", + "WATCHING": "觀看中", + "UNWATCH": "Unwatch", + "WATCHERS": "Watchers", + "BUTTON_TITLE": "Watch/Unwatch this item", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}" + }, + "VOTE_BUTTON": { + "UPVOTE": "Upvote", + "UPVOTED": "Upvoted", + "DOWNVOTE": "Downvote", + "VOTERS": "Voters", + "BUTTON_TITLE": "Upvote/Downvote this item", + "COUNTER_TITLE": "{total, plural, one{one vote} other{# votes}}" + }, "CUSTOM_ATTRIBUTES": { "CUSTOM_FIELDS": "客製化欄位", "SAVE": "儲存客製化欄位", @@ -307,12 +325,10 @@ "CHANGE_PASSWORD_RECOVERY_FORM": { "TITLE": "創建新Taiga通過", "SUBTITLE": "你也許該吃點鐵質豐富的食物,它對你的大腦有益處:p", - "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "回復密碼代碼 ", - "LINK_NEED_TOKEN": "需要一個?", - "TITLE_LINK_NEED_TOKEN": "你是否需要代碼來恢復你所忘記的密碼?", "PLACEHOLDER_NEW_PASSWORD": "新密碼 ", "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "重新輸入新密碼", "ACTION_RESET_PASSWORD": "重設密碼 ", + "ERROR": "Our Oompa Loompas can't find your request to recover your password. Try to ask for it again.", "SUCCESS": "系統已儲存你的新密碼
試試 用它登入 " }, "INVITATION": { @@ -320,14 +336,15 @@ "PAGE_DESCRIPTION": "接受邀請加入Taiga上的專案,它是一個給新創團隊與敏捷開發者設計師使用的專案管理平台。Taiga是一個簡易輕鬆美觀的工具,讓工作變成樂趣。" }, "INVITATION_LOGIN_FORM": { - "NOT_FOUND": "Ooops, 有點問題
我們的系統無法找到你的邀請信", + "NOT_FOUND": "Our Oompa Loompas can't find your invitation.", "SUCCESS": "你成功地加入此專案,歡迎來到 {{project_name}}", "ERROR": "按我們的記錄,你尚未註冊或是未輸入有效的密碼 " }, "HOME": { "PAGE_TITLE": "首頁 - Taiga", "PAGE_DESCRIPTION": "Taiga 首頁,你的主要專案以及任命,觀看使用者故事,任務與問題。", - "EMPTY_WATCHING": "追踪 專案, 使用者故事, 任務, 問題......任何你想知道的資訊 :)", + "EMPTY_WORKING_ON": "It feels empty, doesn't it? Start working with Taiga and you'll see here the stories, tasks and issues you are workin on.", + "EMPTY_WATCHING": "Follow User Stories, Tasks, Issues... that you want to know about :)", "EMPTY_PROJECT_LIST": "你尚無任何專案", "WORKING_ON_SECTION": "進行中", "WATCHING_SECTION": "觀看中" @@ -343,7 +360,7 @@ "DESCRIPTION": "輸入一段簡短描述", "DEPRECATED": "(被棄用)", "DEPRECATED_FILE": "棄用?", - "ADD": "加入新附件 <%- maxFileSizeMsg %>", + "ADD": "加入新附件 {{maxFileSizeMsg}}", "MAX_FILE_SIZE": "[Max. size: {{maxFileSize}}]", "SHOW_DEPRECATED": "+ 顯示棄用的附件", "HIDE_DEPRECATED": "+ 隱藏棄用的附件", @@ -407,15 +424,20 @@ "MEETUP": "符合", "MEETUP_DESCRIPTION": "選擇你的視訊系統。有些開發者需要面對面接觸", "SELECT_VIDEOCONFERENCE": "選擇一個視訊會議系統 ", - "SALT_CHAT_ROOM": "你可以把聊天室名稱加上salt code亂數密碼 " + "SALT_CHAT_ROOM": "你可以把聊天室名稱加上salt code亂數密碼 ", + "JITSI_CHAT_ROOM": "Jitsi", + "APPEARIN_CHAT_ROOM": "AppearIn", + "TALKY_CHAT_ROOM": "Talky", + "CUSTOM_CHAT_ROOM": "自定", + "URL_CHAT_ROOM": "聊天室之網址" }, "PROJECT_PROFILE": { "PAGE_TITLE": "{{sectionName}} - 專案檔案 - {{projectName}}", "PROJECT_DETAILS": "專案細節", "PROJECT_NAME": "專案名稱", "PROJECT_SLUG": "專案代稱", - "NUMBER_SPRINTS": "衝刺任務數目 ", - "NUMBER_US_POINTS": "使用者故事點數數目", + "NUMBER_SPRINTS": "Number of sprints (0 for an undetermined quantity)", + "NUMBER_US_POINTS": "Number of US points (0 for an undetermined quantity)", "TAGS": "標籤", "DESCRIPTION": "描述", "PUBLIC_PROJECT": "公開專案", @@ -425,7 +447,7 @@ "REPORTS": { "TITLE": "Reports", "SUBTITLE": "以 CSV 格式匯出你的專案資料,並製作你的專屬報告", - "DESCRIPTION": "下載CSV檔案或是複製這個生成的網址到你愛用的文字編輯器或電子表格上製作自己的專案檔案資料報告。你可能輕易地視覺化或分析這些資料。", + "DESCRIPTION": "下載CSV檔案格式是複制生成網址到常用的文字編輯軟體/試算表,以保存你的專案資料報告。你可以輕易地視覺化和分析你的資料。", "HELP": "使用者故事預設欄位", "REGENERATE_TITLE": "改變網址", "REGENERATE_SUBTITLE": "你將要改變CSV資料的連結網址,之前的網址將失效。你確定要這樣做嗎?" @@ -448,7 +470,10 @@ "TASK_DESCRIPTION": "任務客製化欄位", "TASK_ADD": "在任務中加入客制欄位", "ISSUE_DESCRIPTION": "問題客製化欄位", - "ISSUE_ADD": "在問題中加入客制欄位" + "ISSUE_ADD": "在問題中加入客制欄位", + "FIELD_TYPE_TEXT": "單行文字", + "FIELD_TYPE_MULTI": "多行", + "FIELD_TYPE_DATE": "Date" }, "PROJECT_VALUES": { "PAGE_TITLE": "{{sectionName}} - 專案數值 - {{projectName}}", @@ -490,6 +515,9 @@ "PAGE_TITLE": "角色- {{projectName}}", "WARNING_NO_ROLE": "注意,你的專案中無角色可以評估使用者故事的點數", "HELP_ROLE_ENABLED": "當啟動時,被指派此角色的成員將可以評估使用者故事點數", + "DISABLE_COMPUTABLE_ALERT_TITLE": "Disable estimation for this role", + "DISABLE_COMPUTABLE_ALERT_SUBTITLE": "If you disable estimation permissions for role {{roleName}} all previous estimations made by this role will be removed", + "DISABLE_COMPUTABLE_ALERT_MESSAGE": "Are you sure you want to disable this role estimations?", "COUNT_MEMBERS": "{{ role.members_count }} 這類角色的成員", "TITLE_DELETE_ROLE": "删除角色", "REPLACEMENT_ROLE": "和此角色有關的使用者都將遭移除 ", @@ -562,6 +590,7 @@ "STATUS_ACTIVE": "活躍 ", "STATUS_PENDING": "待辦中", "DELETE_MEMBER": "刪除成員", + "RESEND": "Resend", "SUCCESS_SEND_INVITATION": "我們已再次發出邀請信給'{{email}}'. ", "ERROR_SEND_INVITATION": "我們未送出邀請 ", "SUCCESS_DELETE": "已刪除 {{message}}.", @@ -626,23 +655,47 @@ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})", "EDIT": "編輯個人資料", "FOLLOW": "追踪", - "PROJECTS": "專案", "CLOSED_US": "閞閉使用者故事", + "PROJECTS": "專案", + "PROJECTS_EMPTY": "{{username}} 尚無專案", "CONTACTS": "聯絡人", - "REPORT": "回報濫用", - "ACTIVITY_TAB": "活動分頁", - "PROJECTS_TAB": "專案分頁", - "CONTACTS_TAB": "聯絡人分頁", - "FAVORITES_TAB": "快速分頁", "CONTACTS_EMPTY": "{{username}} 尚無聯絡人", "CURRENT_USER_CONTACTS_EMPTY": "您尚無任何聯絡人", "CURRENT_USER_CONTACTS_EMPTY_EXPLAIN": "你在Taiga一同工作的伙伴將自動成為你的聯絡人", - "PROJECTS_EMPTY": "{{username}} 尚無專案" + "REPORT": "回報濫用", + "TABS": { + "ACTIVITY_TAB": "動態", + "ACTIVITY_TAB_TITLE": "Show all the activity of this user", + "PROJECTS_TAB": "專案", + "PROJECTS_TAB_TITLE": "List all projects that this user is a member", + "LIKES_TAB": "Likes", + "LIKES_TAB_TITLE": "List all likes made by this user", + "VOTES_TAB": "投票數", + "VOTES_TAB_TITLE": "List all votes made by this user", + "WATCHED_TAB": "Watched", + "WATCHED_TAB_TITLE": "List all item watched by this user", + "CONTACTS_TAB": "聯絡人", + "CONTACTS_TAB_TITLE": "List all contacts made by this user" + } }, "PROFILE_SIDEBAR": { "TITLE": "個人資訊", "DESCRIPTION": "人們可以看到你做的成果以及其進度。加入一段描述介紹以強化你的資料。", - "ADD_INFO": "編輯個人介紹" + "ADD_INFO": "Edit bio" + }, + "PROFILE_FAVS": { + "FILTER_INPUT_PLACEHOLDER": "Type something...", + "FILTER_TYPE_ALL": "所有", + "FILTER_TYPE_ALL_TITLE": "Show all", + "FILTER_TYPE_PROJECTS": "專案", + "FILTER_TYPE_PROJECT_TITLES": "Show only projects", + "FILTER_TYPE_USER_STORIES": "Stories", + "FILTER_TYPE_USER_STORIES_TITLES": "Show only user stories", + "FILTER_TYPE_TASKS": "任務 ", + "FILTER_TYPE_TASK_TITLES": "Show only tasks", + "FILTER_TYPE_ISSUES": "問題 ", + "FILTER_TYPE_ISSUES_TITLE": "Show only issues", + "EMPTY_TITLE": "It looks like there's nothing to show here." } }, "PROJECT": { @@ -711,6 +764,27 @@ "ERROR_MESSAGE": "我們的系統無法滙入你的資料", "ERROR_MAX_SIZE_EXCEEDED": "'{{fileName}}' ({{fileSize}}) 超過系統容量上限, 請重傳小一點的檔案 ({{maxFileSize}})", "SYNC_SUCCESS": "你的專案已成功滙入" + }, + "LIKE_BUTTON": { + "LIKE": "Like", + "LIKED": "Liked", + "UNLIKE": "Unlike", + "BUTTON_TITLE": "Like or unlike this project", + "COUNTER_TITLE": "{total, plural, one{one fan} other{# fans}}" + }, + "WATCH_BUTTON": { + "BUTTON_TITLE": "Watch this project and set notification policy", + "WATCH": "Watch", + "WATCHING": "觀看中", + "COUNTER_TITLE": "{total, plural, one{one watcher} other{# watchers}}", + "OPTIONS": { + "NOTIFY_ALL": "Receive all notifications", + "NOTIFY_ALL_TITLE": "Receive all notifications for this project", + "NOTIFY_INVOLVED": "Only involved", + "NOTIFY_INVOLVED_TITLE": "Recive notificacions only when you are involved", + "UNWATCH": "Unwatch", + "UNWATCH_TITLE": "Unwatch this project" + } } }, "LIGHTBOX": { @@ -783,7 +857,7 @@ "SECTION_NAME": "使用者故事細節", "LINK_TASKBOARD": "任務板", "TITLE_LINK_TASKBOARD": "到任務板去", - "TOTAL_POINTS": "全部", + "TOTAL_POINTS": "total points", "ADD": "+新增使用者故事", "ADD_BULK": "批次加入新使用者故事", "PROMOTED": "此使用者故事已提昇成問題:", @@ -870,6 +944,10 @@ "PAGE_TITLE": "待辦任務優先表 - {{projectName}}", "PAGE_DESCRIPTION": "待辦任務優先表面板 {{projectName}}(專案的使用者故事與衝刺任務): {{projectDescription}}", "SECTION_NAME": "待辦任務優先表", + "CUSTOMIZE_GRAPH": "Customize your backlog graph", + "CUSTOMIZE_GRAPH_TEXT": "To have a nice graph that helps you follow the evolution of the project you have to set up the points and sprints through the", + "CUSTOMIZE_GRAPH_ADMIN": "管理者", + "CUSTOMIZE_GRAPH_TITLE": "Set up the points and sprints through the Admin", "MOVE_US_TO_CURRENT_SPRINT": "移到目前的 Sprint", "SHOW_FILTERS": "顯示過濾器", "SHOW_TAGS": "顯示標籤 (Tag)", @@ -889,10 +967,10 @@ "CHART": { "XAXIS_LABEL": "衝刺任務", "YAXIS_LABEL": "點數", - "OPTIMAL": "最佳給衝刺任務待辦點數 {{xval}} 應是 {{yval}}", - "REAL": "任務衝刺實際待辦點數 {{xval}} 為 {{yval}}", - "INCREMENT_TEAM": "團隊要求的任務衝刺逐步新增點數 {{xval}} 為 {{yval}}", - "INCREMENT_CLIENT": "客戶要求的任務衝刺逐步新增點數 {{xval}} 為 {{yval}}" + "OPTIMAL": "最適化的衝刺任務\"{{sprintName}}\"待定點數應為 {{value}}", + "REAL": "真實的衝刺任務\"{{sprintName}}\"待定點數為 {{value}}", + "INCREMENT_TEAM": "團隊要求的衝刺任務\"{{sprintName}}\"漸增點數為 {{value}}", + "INCREMENT_CLIENT": "客戶要求的衝刺任務\"{{sprintName}}\"漸增點數為 {{value}}" }, "TAGS": { "TOGGLE": "切換標籤可見度", @@ -909,7 +987,8 @@ "OPEN_TASKS": "開啟
任務", "CLOSED_TASKS": "已關閉
任務", "IOCAINE_DOSES": "毒物(全新任務挑戰)
劑量", - "SHOW_STATISTICS_TITLE": "顯示統計" + "SHOW_STATISTICS_TITLE": "顯示統計", + "TOGGLE_BAKLOG_GRAPH": "顯示/隱藏 剩餘工作量圖" }, "SUMMARY": { "PROJECT_POINTS": "專案
點數", @@ -932,19 +1011,20 @@ "LINK_TASKBOARD": "衝刺任務板", "TITLE_LINK_TASKBOARD": "到任務板 {{spring.name}}", "NUMBER_SPRINTS": "
衝刺任務 ", - "TITLE_ACTION_NEW_SPRINT": "+ 新衝刺任務 ", - "ACTION_NEW_SPRINT": "+ 新衝刺任務 ", + "EMPTY": "YOU HAVE NO SPRINTS", + "WARNING_EMPTY_SPRINT": "Drop here Stories from your backlog to start a new sprint", + "TITLE_ACTION_NEW_SPRINT": "Add new sprint", + "TEXT_ACTION_NEW_SPRINT": "You may want to create a new sprint in your project", "ACTION_SHOW_CLOSED_SPRINTS": "顯示關閉衝刺任務", "ACTION_HIDE_CLOSED_SPRINTS": "隱藏衝刺任務" } }, "ERROR": { "TEXT1": "系統出了一點問題,工程師正在搶修中", - "TEXT2": "請再重新載入", "NOT_FOUND": "找不到", "NOT_FOUND_TEXT": "Error 404,你要找的網頁不存在,你可以稍後再回來TAIGA首頁,看看是否能再次找到要找的東西。", "PERMISSION_DENIED": "無此權限", - "PERMISSION_DENIED_CODE": "Error 403.", + "PERMISSION_DENIED_TEXT": "You don't have permission to access to this page.", "VERSION_ERROR": "Taiga某人之前更改了這個,而我們的工程師無法再做改變。請重新載入頁面來使用你的更新(之前設定將消失)" }, "TASKBOARD": { @@ -955,6 +1035,8 @@ "TITLE_ACTION_ADD_BULK": "批次加入新任務 ", "TITLE_ACTION_ASSIGN": "指派任務 ", "TITLE_ACTION_EDIT": "結束任務 ", + "PLACEHOLDER_CARD_TITLE": "This could be a task", + "PLACEHOLDER_CARD_TEXT": "Split Stories into tasks to track them separately", "TABLE": { "COLUMN": "使用者故事", "TITLE_ACTION_FOLD": "隱藏欄位", @@ -1073,16 +1155,17 @@ "SEVERITY": "急迫性", "PRIORITY": "優先性", "SUBJECT": "主旨", + "VOTES": "投票數", "STATUS": "狀態", "CREATED": "已創建", "ASSIGNED_TO": "指派給 " }, "TITLE_ACTION_CHANGE_STATUS": "改變狀態", "TITLE_ACTION_ASSIGNED_TO": "指派給 ", + "BLOCKED": "已封鎖", "EMPTY": { "TITLE": "沒有問題回報:-)", - "SUBTITLE": "你有發現任何問題嗎", - "ACTION_CREATE_ISSUE": "創建一個新問題" + "SUBTITLE": "你有發現任何問題嗎" } } }, @@ -1104,7 +1187,9 @@ "ACTION_HIDE_ARCHIVED": "隱藏歸檔", "HIDDEN_USER_STORIES": "此狀態下的使用者故事預設為隱藏", "ARCHIVED": "你已歸檔", - "UNDO_ARCHIVED": "拖移 & 丟到未做" + "UNDO_ARCHIVED": "拖移 & 丟到未做", + "PLACEHOLDER_CARD_TITLE": "These are your User Stories", + "PLACEHOLDER_CARD_TEXT": "Stories might also have subtasks to separate requirements" }, "SEARCH": { "PAGE_TITLE": "搜尋 - {{projectName}}", @@ -1182,7 +1267,9 @@ "BIO": "自介(最多210字)", "PLACEHOLDER_BIO": "請自我介紹", "LANGUAGE": "語言", - "LANGUAGE_DEFAULT": "-- 使用設預語言 -- " + "LANGUAGE_DEFAULT": "-- 使用設預語言 -- ", + "THEME": "主題", + "THEME_DEFAULT": "-- 使用預設主題--" } }, "WIZARD": { @@ -1234,18 +1321,91 @@ "NEW_PROJECT": "{{username}} 創建專案 {{project_name}}", "MILESTONE_UPDATED": "{{username}}更新衝刺任務 {{obj_name}} ", "US_UPDATED": "{{username}} 已更新 {{obj_name}}使用者故事之 \"{{field_name}}\"屬性。", + "US_UPDATED_WITH_NEW_VALUE": "{{username}} 已更新了 {{obj_name}} 使用者故事的\"{{field_name}}\"屬性到{{new_value}}", + "US_UPDATED_POINTS": "{{username}} 已更新了 {{obj_name}} 的使用者故事\"{{field_name}}\"點數為{{new_value}}", "ISSUE_UPDATED": "{{username}} 更新了{{obj_name}}問題的 \"{{field_name}}\" 屬性", - "TASK_UPDATED": "{{username}}更新了任務 {{obj_name}}之 \"{{field_name}}\" 屬性", + "ISSUE_UPDATED_WITH_NEW_VALUE": "{{username}} 已更新了 {{obj_name}} 問題的\"{{field_name}}\"屬性到{{new_value}}", + "TASK_UPDATED": "{{username}} 已更新了 {{obj_name}} 使用者故事的\"{{field_name}}\"屬性到{{new_value}}", + "TASK_UPDATED_WITH_NEW_VALUE": "{{username}} 已更新了 {{obj_name}} 任務\"{{field_name}}\"的屬性到{{new_value}}", "TASK_UPDATED_WITH_US": "{{username}} 更新了 {{obj_name}} 任務之\"{{field_name}}\" 屬性,其為 {{us_name}} 之使用者故事", + "TASK_UPDATED_WITH_US_NEW_VALUE": "{{username}} 已更新了 {{obj_name}} 下的 {{us_name}} 使用者故事\"{{field_name}}\"屬性到{{new_value}}", "WIKI_UPDATED": "\n{{username}} 更新了維基頁{{obj_name}}", "NEW_COMMENT_US": "{{username}} 評論了 {{obj_name}}使用者故事", "NEW_COMMENT_ISSUE": "{{username}}評論了此問題 {{obj_name}}", "NEW_COMMENT_TASK": "{{username}} 評論了此任務{{obj_name}}", "NEW_MEMBER": "{{project_name}} 有新成員", "US_ADDED_MILESTONE": "{{username}} 增加使用者故事 {{obj_name}} 給 {{sprint_name}}", + "US_MOVED": "{{username}} has moved the US {{obj_name}}", "US_REMOVED_FROM_MILESTONE": "{{username}} 新增使用者故事 {{obj_name}} 到待辦工作優先列表", "BLOCKED": "{{username}} 封鎖了 {{obj_name}}", "UNBLOCKED": "{{username}} 解除了封鎖 {{obj_name}}", "NEW_USER": "{{username}} 已加入 Taiga" + }, + "LEGAL": { + "TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD": "By clicking \"Sign up\"', you agree to our
terms of service and privacy policy." + }, + "EXTERNAL_APP": { + "PAGE_TITLE": "An external app requires authentication", + "PAGE_DESCRIPTION": "An external app requires authentication", + "AUTHORIZATION_REQUEST": "Authorize {{application}} to use your Taiga account?", + "LOGIN_WITH_ANOTHER_USER": "Login with another user", + "AUTHORIZE_APP": "Authorize app", + "CANCEL": "取消" + }, + "JOYRIDE": { + "DASHBOARD": { + "STEP1": { + "TITLE": "你的專案", + "TEXT": "Welcome! Here you will find the projects you are involved on. We have left you sample project templates to help you discover the power of Taiga." + }, + "STEP2": { + "TITLE": "進行中", + "TEXT": "Here you will find the User Stories, Tasks and Issues in which you are working on." + }, + "STEP3": { + "TITLE": "觀看中", + "TEXT1": "And right here you will find the ones that you want to know about.", + "TEXT2": "You are already working with Taiga ;)" + }, + "STEP4": { + "TITLE": "Let’s start", + "TEXT1": "You can start by creating your first Taiga project.", + "TEXT2": "Good luck!" + } + }, + "BACKLOG": { + "STEP1": { + "TITLE": "Project summary", + "TEXT1": "Here you will see the state of your project.", + "TEXT2": "You can change every kind of project settings through the admin." + }, + "STEP2": { + "TITLE": "Product backlog", + "TEXT": "The backlog is the list of requirements (User Stories) for the project. Here is where you will plan your sprints." + }, + "STEP3": { + "TITLE": "衝刺任務", + "TEXT": "Sprints are short periods of time (usually 2 weeks) during which specific work has to be completed and delivered." + }, + "STEP4": { + "TITLE": "使用者故事", + "TEXT": "Those are the requirements at high level. You can add them to the backlog and drag them to the sprint in which it should be delivered." + } + }, + "KANBAN": { + "STEP1": { + "TITLE": "Customize your workflow", + "TEXT": "Set up the columns you need to map your workflow statuses through the admin." + }, + "STEP2": { + "TITLE": "User Stories & Tasks", + "TEXT": "User Stories are the requirements at high level. You can drag them to different columns." + }, + "STEP3": { + "TITLE": "Adding User Stories", + "TEXT1": "You may want to add a single User Story (add US icon) or a group of them (bulk icon)", + "TEXT2": "Good luck!" + } + } } } \ No newline at end of file diff --git a/app/modules/components/joy-ride/joy-ride.directive.coffee b/app/modules/components/joy-ride/joy-ride.directive.coffee new file mode 100644 index 00000000..8b482811 --- /dev/null +++ b/app/modules/components/joy-ride/joy-ride.directive.coffee @@ -0,0 +1,84 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: joy-ride.directive.coffee +### + +taiga = @.taiga + +JoyRideDirective = ($rootScope, currentUserService, joyRideService, $location) -> + link = (scope, el, attrs, ctrl) -> + unsuscribe = null + intro = introJs() + + #Todo: translate + intro.setOptions({ + exitOnEsc: false, + exitOnOverlayClick: false, + showStepNumbers: false, + nextLabel: 'Next →', + prevLabel: '← Back', + skipLabel: 'Skip', + doneLabel: 'Done', + disableInteraction: true + }) + + intro.oncomplete () -> + $('html,body').scrollTop(0) + + intro.onexit () -> + currentUserService.disableJoyRide() + + initJoyrRide = (next, config) -> + if !config[next.joyride] + return + + intro.setOption('steps', joyRideService.get(next.joyride)) + intro.start() + + $rootScope.$on '$routeChangeSuccess', (event, next) -> + if !next.joyride || !currentUserService.isAuthenticated() + intro.exit() + unsuscribe() if unsuscribe + return + + + intro.oncomplete () -> + currentUserService.disableJoyRide(next.joyride) + + if next.loader + unsuscribe = $rootScope.$on 'loader:end', () -> + currentUserService.loadJoyRideConfig() + .then (config) -> initJoyrRide(next, config) + + unsuscribe() + else + currentUserService.loadJoyRideConfig() + .then (config) -> initJoyrRide(next, config) + + return { + scope: {}, + link: link + } + +JoyRideDirective.$inject = [ + "$rootScope", + "tgCurrentUserService", + "tgJoyRideService", + "$location" +] + +angular.module("taigaComponents").directive("tgJoyRide", JoyRideDirective) diff --git a/app/modules/components/joy-ride/joy-ride.service.coffee b/app/modules/components/joy-ride/joy-ride.service.coffee new file mode 100644 index 00000000..46fb5627 --- /dev/null +++ b/app/modules/components/joy-ride/joy-ride.service.coffee @@ -0,0 +1,175 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: joy-ride.service.coffee +### + +class JoyRideService extends taiga.Service + @.$inject = [ + '$translate', + 'tgCheckPermissionsService' + ] + + constructor: (@translate, @checkPermissionsService) -> + + getConfig: () -> + return { + dashboard: () => + steps = [ + { + element: '.project-list > section:not(.ng-hide)', + position: 'left', + joyride: { + title: @translate.instant('JOYRIDE.DASHBOARD.STEP1.TITLE'), + text: @translate.instant('JOYRIDE.DASHBOARD.STEP1.TEXT') + } + }, + { + element: '.working-on-container', + position: 'right', + joyride: { + title: @translate.instant('JOYRIDE.DASHBOARD.STEP2.TITLE'), + text: @translate.instant('JOYRIDE.DASHBOARD.STEP2.TEXT') + } + }, + { + element: '.watching-container', + position: 'right', + joyride: { + title: @translate.instant('JOYRIDE.DASHBOARD.STEP3.TITLE') + text: [ + @translate.instant('JOYRIDE.DASHBOARD.STEP3.TEXT1'), + @translate.instant('JOYRIDE.DASHBOARD.STEP3.TEXT2') + ] + } + } + ] + + if !$('.project-list .create-project-button').is(':hidden') + steps.push({ + element: '.project-list .create-project-button', + position: 'bottom', + joyride: { + title: @translate.instant('JOYRIDE.DASHBOARD.STEP4.TITLE') + text: [ + @translate.instant('JOYRIDE.DASHBOARD.STEP4.TEXT1'), + @translate.instant('JOYRIDE.DASHBOARD.STEP4.TEXT2') + ] + } + }) + + return steps + + backlog: () => + steps = [ + { + element: '.summary', + position: 'bottom', + joyride: { + title: @translate.instant('JOYRIDE.BACKLOG.STEP1.TITLE') + text: [ + @translate.instant('JOYRIDE.BACKLOG.STEP1.TEXT1'), + @translate.instant('JOYRIDE.BACKLOG.STEP1.TEXT2') + ] + } + }, + { + element: '.backlog-table-empty', + position: 'bottom', + joyride: { + title: @translate.instant('JOYRIDE.BACKLOG.STEP2.TITLE') + text: @translate.instant('JOYRIDE.BACKLOG.STEP2.TEXT') + } + }, + { + element: '.sprints', + position: 'left', + joyride: { + title: @translate.instant('JOYRIDE.BACKLOG.STEP3.TITLE') + text: @translate.instant('JOYRIDE.BACKLOG.STEP3.TEXT') + } + } + ] + + if @checkPermissionsService.check('add_us') + steps.push({ + element: '.new-us', + position: 'rigth', + joyride: { + title: @translate.instant('JOYRIDE.BACKLOG.STEP4.TITLE') + text: @translate.instant('JOYRIDE.BACKLOG.STEP4.TEXT') + } + }) + + return steps + + kanban: () => + steps = [ + { + element: '.kanban-table-inner', + position: 'bottom', + joyride: { + title: @translate.instant('JOYRIDE.KANBAN.STEP1.TITLE') + text: @translate.instant('JOYRIDE.KANBAN.STEP1.TEXT') + } + }, + { + element: '.card-placeholder', + position: 'right', + joyride: { + title: @translate.instant('JOYRIDE.KANBAN.STEP2.TITLE') + text: @translate.instant('JOYRIDE.KANBAN.STEP2.TEXT') + } + } + ] + + if @checkPermissionsService.check('add_us') + steps.push({ + element: '.icon-plus', + position: 'bottom', + joyride: { + title: @translate.instant('JOYRIDE.KANBAN.STEP3.TITLE') + text: [ + @translate.instant('JOYRIDE.KANBAN.STEP3.TEXT1'), + @translate.instant('JOYRIDE.KANBAN.STEP3.TEXT2'), + ] + } + }) + + return steps + } + + get: (name) -> + joyRides = @.getConfig() + joyRide = joyRides[name].call(this) + + return _.map joyRide, (item) -> + html = "" + + if item.joyride.title + html += "

#{item.joyride.title}

" + + if _.isArray(item.joyride.text) + _.forEach item.joyride.text, (text) -> + html += "

#{text}

" + else + html += "

#{item.joyride.text}

" + + item.intro = html + + return item + +angular.module("taigaComponents").service("tgJoyRideService", JoyRideService) diff --git a/app/modules/components/joy-ride/joy-ride.service.spec.coffee b/app/modules/components/joy-ride/joy-ride.service.spec.coffee new file mode 100644 index 00000000..48e1abbe --- /dev/null +++ b/app/modules/components/joy-ride/joy-ride.service.spec.coffee @@ -0,0 +1,76 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: joy-ride.service.spec.coffee +### + +describe "tgJoyRideService", -> + joyRideService = provide = null + mocks = {} + + _mockTranslate = () -> + mocks.translate = { + instant: sinon.stub() + } + + provide.value "$translate", mocks.translate + + _mockCheckPermissionsService = () -> + mocks.checkPermissionsService = { + check: sinon.stub() + } + + mocks.checkPermissionsService.check.returns(true) + + provide.value "tgCheckPermissionsService", mocks.checkPermissionsService + + _inject = (callback) -> + inject (_tgJoyRideService_) -> + joyRideService = _tgJoyRideService_ + callback() if callback + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTranslate() + _mockCheckPermissionsService() + return null + + _setup = -> + _mocks() + + beforeEach -> + module "taigaComponents" + _setup() + _inject() + + it "get joyride by category", () -> + example = { + element: '.project-list > section:not(.ng-hide)', + position: 'left', + joyride: { + title: 'test', + text: 'test' + }, + intro: '

test

test

' + } + + mocks.translate.instant.returns('test') + + joyRide = joyRideService.get('dashboard') + + expect(joyRide).to.have.length(4) + expect(joyRide[0]).to.be.eql(example) diff --git a/app/modules/components/project-menu/project-menu.controller.coffee b/app/modules/components/project-menu/project-menu.controller.coffee index 335ce2dc..be5f9d00 100644 --- a/app/modules/components/project-menu/project-menu.controller.coffee +++ b/app/modules/components/project-menu/project-menu.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: project-menu.controller.coffee +### + class ProjectMenuController @.$inject = [ "tgProjectService", @@ -81,14 +100,16 @@ class ProjectMenuController baseUrl = "https://talky.io/" else if @.project.get("videoconferences") == "jitsi" baseUrl = "https://meet.jit.si/" - url = @.project.get("slug") + "-" + taiga.slugify(@.project.get("videoconferences_salt")) + url = @.project.get("slug") + "-" + taiga.slugify(@.project.get("videoconferences_extra_data")) url = url.replace(/-/g, "") return baseUrl + url + else if @.project.get("videoconferences") == "custom" + return @.project.get("videoconferences_extra_data") else return "" - if @.project.get("videoconferences_salt") - url = @.project.get("slug") + "-" + @.project.get("videoconferences_salt") + if @.project.get("videoconferences_extra_data") + url = @.project.get("slug") + "-" + @.project.get("videoconferences_extra_data") else url = @.project.get("slug") diff --git a/app/modules/components/project-menu/project-menu.controller.spec.coffee b/app/modules/components/project-menu/project-menu.controller.spec.coffee index 89f1f650..bb717b78 100644 --- a/app/modules/components/project-menu/project-menu.controller.spec.coffee +++ b/app/modules/components/project-menu/project-menu.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: project-menu.controller.spec.coffee +### + describe "ProjectMenu", -> $provide = null $controller = null @@ -63,7 +82,7 @@ describe "ProjectMenu", -> it "videoconference url", () -> project = Immutable.fromJS({ "videoconferences": "appear-in", - "videoconferences_salt": "123", + "videoconferences_extra_data": "123", "slug": "project-slug" }) diff --git a/app/modules/components/project-menu/project-menu.directive.coffee b/app/modules/components/project-menu/project-menu.directive.coffee index d465ddd7..369bb546 100644 --- a/app/modules/components/project-menu/project-menu.directive.coffee +++ b/app/modules/components/project-menu/project-menu.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: project-menu.directive.coffee +### + taiga = @.taiga ProjectMenuDirective = (projectService, lightboxFactory) -> diff --git a/app/modules/components/project-menu/project-menu.jade b/app/modules/components/project-menu/project-menu.jade index a98077f1..9ff22d8e 100644 --- a/app/modules/components/project-menu/project-menu.jade +++ b/app/modules/components/project-menu/project-menu.jade @@ -2,46 +2,92 @@ nav.menu(ng-if="vm.project") div(class="menu-container") ul(class="main-nav") li(id="nav-search") - a(href="", ng-class="{active: vm.active == 'search'}", title="{{'PROJECT.SECTION.SEARCH' | translate}}", tabindex="1", ng-click="vm.search()") - span(class="icon icon-search") + a( + href="" + ng-click="vm.search()" + ng-class="{active: vm.active == 'search'}" + aria-label="{{'PROJECT.SECTION.SEARCH' | translate}}" + tabindex="1" + ) + span.icon.icon-search span.helper(translate="PROJECT.SECTION.SEARCH") li(id="nav-timeline") - a(ng-class="{active: vm.active == 'project-timeline'}", title="{{'PROJECT.SECTION.TIMELINE' | translate}}", tg-nav="project:project=vm.project.get('slug')", tabindex="2") + a( + tg-nav="project:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'project-timeline'}" + aria-label="{{'PROJECT.SECTION.TIMELINE' | translate}}" + tabindex="2" + ) include ../../../svg/timeline.svg span.helper(translate="PROJECT.SECTION.TIMELINE") li(id="nav-backlog", ng-if="vm.menu.get('backlog')") - a(ng-class="{active: vm.active == 'backlog'}", title="{{'PROJECT.SECTION.BACKLOG' | translate}}", tg-nav="project-backlog:project=vm.project.get('slug')", tabindex="2") - span(class="icon icon-backlog") + a( + tg-nav="project-backlog:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'backlog'}" + aria-label="{{'PROJECT.SECTION.BACKLOG' | translate}}" + tabindex="2" + ) + span.icon.icon-scrum span.helper(translate="PROJECT.SECTION.BACKLOG") li(id="nav-kanban", ng-if="vm.menu.get('kanban')") - a(ng-class="{active: vm.active == 'kanban'}", title="{{'PROJECT.SECTION.KANBAN' | translate}}", tg-nav="project-kanban:project=vm.project.get('slug')", tabindex="3") - span(class="icon icon-kanban") + a( + tg-nav="project-kanban:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'kanban'}" + aria-label="{{'PROJECT.SECTION.KANBAN' | translate}}" + tabindex="3" + ) + span.icon.icon-kanban span.helper(translate="PROJECT.SECTION.KANBAN") li(id="nav-issues", ng-if="vm.menu.get('issues')") - a(ng-class="{active: vm.active == 'issues'}", title="{{'PROJECT.SECTION.ISSUES' | translate}}", tg-nav="project-issues:project=vm.project.get('slug')", tabindex="4") - span(class="icon icon-issues") + a( + tg-nav="project-issues:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'issues'}" + aria-label="{{'PROJECT.SECTION.ISSUES' | translate}}" + tabindex="4" + ) + span.icon.icon-issues span.helper(translate="PROJECT.SECTION.ISSUES") li(id="nav-wiki", ng-if="vm.menu.get('wiki')") - a(ng-class="{active: vm.active == 'wiki'}", title="{{'PROJECT.SECTION.WIKI' | translate}}", tg-nav="project-wiki:project=vm.project.get('slug')", tabindex="5") - span(class="icon icon-wiki") + a( + tg-nav="project-wiki:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'wiki'}" + aria-label="{{'PROJECT.SECTION.WIKI' | translate}}" + tabindex="5" + ) + span.icon.icon-wiki span.helper(translate="PROJECT.SECTION.WIKI") li(id="nav-team") - a(ng-class="{active: vm.active == 'team'}", title="{{'PROJECT.SECTION.TEAM' | translate}}", tg-nav="project-team:project=vm.project.get('slug')", tabindex="6") + a( + tg-nav="project-team:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'team'}" + aria-label="{{'PROJECT.SECTION.TEAM' | translate}}" + tabindex="6" + ) span(class="icon icon-team") span.helper(translate="PROJECT.SECTION.TEAM") li(id="nav-video", ng-if="vm.project.get('videoconferenceUrl')") - a(ng-href="{{vm.project.get('videoconferenceUrl')}}", target="_blank", title="{{'PROJECT.SECTION.MEETUP' | translate}}", tabindex="7") - span(class="icon icon-video") + a( + ng-href="{{vm.project.get('videoconferenceUrl')}}" + target="_blank" + aria-label="{{'PROJECT.SECTION.MEETUP' | translate}}" + tabindex="7" + ) + span.icon.icon-video span.helper(translate="PROJECT.SECTION.MEETUP") li(id="nav-admin", ng-if="vm.project.get('i_am_owner')") - a(ng-class="{active: vm.active == 'admin'}", tg-nav="project-admin-home:project=vm.project.get('slug')", title="{{'PROJECT.SECTION.ADMIN' | translate}}", tabindex="8") - span(class="icon icon-settings") + a( + tg-nav="project-admin-home:project=vm.project.get('slug')" + ng-class="{active: vm.active == 'admin'}" + aria-label="{{'PROJECT.SECTION.ADMIN' | translate}}" + tabindex="8" + ) + span.icon.icon-settings span.helper(translate="PROJECT.SECTION.ADMIN") diff --git a/app/modules/components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.directive.coffee b/app/modules/components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.directive.coffee new file mode 100644 index 00000000..1489390a --- /dev/null +++ b/app/modules/components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.directive.coffee @@ -0,0 +1,39 @@ +### +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.directive.coffee +### + + +TermsOfServiceAndPrivacyPolicyNoticeDirective = ($config) -> + link = (scope, el, attrs) -> + scope.privacyPolicyUrl = $config.get("privacyPolicyUrl") + scope.termsOfServiceUrl = $config.get("termsOfServiceUrl") + + return { + restrict: "AE", + scope: {}, + link: link, + templateUrl: "components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.html" + } + +angular.module("taigaComponents") + .directive("tgTermsOfServiceAndPrivacyPolicyNotice", [ + "$tgConfig", + TermsOfServiceAndPrivacyPolicyNoticeDirective + ]) diff --git a/app/modules/components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.jade b/app/modules/components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.jade new file mode 100644 index 00000000..c609f5dd --- /dev/null +++ b/app/modules/components/terms-of-service-and-privacy-policy-notice/terms-of-service-and-privacy-policy-notice.jade @@ -0,0 +1,5 @@ +p.register-text( + ng-if="privacyPolicyUrl && termsOfServiceUrl" + translate="LEGAL.TERMS_OF_SERVICE_AND_PRIVACY_POLICY_AD" + translate-values="{termsOfServiceUrl: termsOfServiceUrl, privacyPolicyUrl: privacyPolicyUrl}" +) diff --git a/app/modules/components/vote-button/vote-button.controller.coffee b/app/modules/components/vote-button/vote-button.controller.coffee new file mode 100644 index 00000000..9016b674 --- /dev/null +++ b/app/modules/components/vote-button/vote-button.controller.coffee @@ -0,0 +1,55 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: vote-button.controller.coffee +### + +class VoteButtonController + @.$inject = [ + "tgCurrentUserService", + ] + + constructor: (@currentUserService) -> + @.user = @currentUserService.getUser() + @.isMouseOver = false + @.loading = false + + showTextWhenMouseIsOver: -> + @.isMouseOver = true + + showTextWhenMouseIsLeave: -> + @.isMouseOver = false + + toggleVote: -> + @.loading = true + + if not @.item.is_voter + promise = @._upvote() + else + promise = @._downvote() + + promise.finally () => @.loading = false + + return promise + + _upvote: -> + @.onUpvote().then => + @.showTextWhenMouseIsLeave() + + _downvote: -> + @.onDownvote() + +angular.module("taigaComponents").controller("VoteButton", VoteButtonController) diff --git a/app/modules/components/vote-button/vote-button.controller.spec.coffee b/app/modules/components/vote-button/vote-button.controller.spec.coffee new file mode 100644 index 00000000..34276b7a --- /dev/null +++ b/app/modules/components/vote-button/vote-button.controller.spec.coffee @@ -0,0 +1,101 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: vote-button.controller.spec.coffee +### + +describe "VoteButton", -> + provide = null + $controller = null + $rootScope = null + mocks = {} + + _mockCurrentUser = () -> + mocks.currentUser = { + getUser: sinon.stub() + } + + provide.value "tgCurrentUserService", mocks.currentUser + + _mocks = -> + mocks = { + onUpvote: sinon.stub(), + onDownvote: sinon.stub() + } + + module ($provide) -> + provide = $provide + _mockCurrentUser() + return null + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaComponents" + _setup() + + it "upvote", (done) -> + $scope = $rootScope.$new() + + mocks.onUpvote = sinon.stub().promise() + + ctrl = $controller("VoteButton", $scope, { + item: {is_voter: false} + onUpvote: mocks.onUpvote + onDownvote: mocks.onDownvote + }) + + promise = ctrl.toggleVote() + + expect(ctrl.loading).to.be.true; + + mocks.onUpvote.resolve() + + promise.finally () -> + expect(mocks.onUpvote).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() + + it "downvote", (done) -> + $scope = $rootScope.$new() + + mocks.onDownvote = sinon.stub().promise() + + ctrl = $controller("VoteButton", $scope, { + item: {is_voter: true} + onUpvote: mocks.onUpvote + onDownvote: mocks.onDownvote + }) + + promise = ctrl.toggleVote() + + expect(ctrl.loading).to.be.true; + + mocks.onDownvote.resolve() + + promise.finally () -> + expect(mocks.onDownvote).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() diff --git a/app/modules/components/vote-button/vote-button.directive.coffee b/app/modules/components/vote-button/vote-button.directive.coffee new file mode 100644 index 00000000..60db1a18 --- /dev/null +++ b/app/modules/components/vote-button/vote-button.directive.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: vote-button.directive.coffee +### + +VoteButtonDirective = -> + return { + scope: {} + controller: "VoteButton", + bindToController: { + item: "=", + onUpvote: "=", + onDownvote: "=" + } + controllerAs: "vm", + templateUrl: "components/vote-button/vote-button.html", + } + +angular.module("taigaComponents").directive("tgVoteButton", VoteButtonDirective) diff --git a/app/modules/components/vote-button/vote-button.jade b/app/modules/components/vote-button/vote-button.jade new file mode 100644 index 00000000..457f966b --- /dev/null +++ b/app/modules/components/vote-button/vote-button.jade @@ -0,0 +1,25 @@ +//- Registered user button +a.vote-inner( + href="" + title="{{ 'COMMON.VOTE_BUTTON.BUTTON_TITLE' | translate }}" + ng-if="::vm.user" + ng-click="vm.toggleVote()" + ng-class="{'active': vm.item.is_voter, 'is-hover': vm.item.is_voter && vm.isMouseOver, 'disable': !vm.user}" + ng-mouseover="vm.showTextWhenMouseIsOver()" + ng-mouseleave="vm.showTextWhenMouseIsLeave()" +) + span.track-icon + include ../../../svg/upvote.svg + span.track-button-counter( + title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.total_voters||0}:'messageformat' }}", + tg-loading="vm.loading" + ) {{ vm.item.total_voters }} + +//- Anonymous user button + +span.vote-inner(ng-if="::!vm.user") + span.track-icon + include ../../../svg/upvote.svg + span.track-button-counter( + title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.total_voters||0}:'messageformat' }}" + ) {{ ::vm.item.total_voters }} diff --git a/app/modules/components/watch-button/watch-button.controller.coffee b/app/modules/components/watch-button/watch-button.controller.coffee new file mode 100644 index 00000000..5a59621d --- /dev/null +++ b/app/modules/components/watch-button/watch-button.controller.coffee @@ -0,0 +1,55 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-button.controller.coffee +### + +class WatchButtonController + @.$inject = [ + "tgCurrentUserService", + ] + + constructor: (@currentUserService) -> + @.user = @currentUserService.getUser() + @.isMouseOver = false + @.loading = false + + showTextWhenMouseIsOver: -> + @.isMouseOver = true + + showTextWhenMouseIsLeave: -> + @.isMouseOver = false + + toggleWatch: -> + @.loading = true + + if not @.item.is_watcher + promise = @._watch() + else + promise = @._unwatch() + + promise.finally () => @.loading = false + + return promise + + _watch: -> + @.onWatch().then => + @.showTextWhenMouseIsLeave() + + _unwatch: -> + @.onUnwatch() + +angular.module("taigaComponents").controller("WatchButton", WatchButtonController) diff --git a/app/modules/components/watch-button/watch-button.controller.spec.coffee b/app/modules/components/watch-button/watch-button.controller.spec.coffee new file mode 100644 index 00000000..b2523f5a --- /dev/null +++ b/app/modules/components/watch-button/watch-button.controller.spec.coffee @@ -0,0 +1,102 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-button.controller.spec.coffee +### + +describe "WatchButton", -> + provide = null + $controller = null + $rootScope = null + mocks = {} + + _mockCurrentUser = () -> + mocks.currentUser = { + getUser: sinon.stub() + } + + provide.value "tgCurrentUserService", mocks.currentUser + + _mocks = -> + mocks = { + onWatch: sinon.stub(), + onUnwatch: sinon.stub() + } + + module ($provide) -> + provide = $provide + _mockCurrentUser() + return null + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaComponents" + _setup() + + it "watch", (done) -> + $scope = $rootScope.$new() + + mocks.onWatch = sinon.stub().promise() + + ctrl = $controller("WatchButton", $scope, { + item: {is_watcher: false} + onWatch: mocks.onWatch + onUnwatch: mocks.onUnwatch + }) + + + promise = ctrl.toggleWatch() + + expect(ctrl.loading).to.be.true; + + mocks.onWatch.resolve() + + promise.finally () -> + expect(mocks.onWatch).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() + + it "unwatch", (done) -> + $scope = $rootScope.$new() + + mocks.onUnwatch = sinon.stub().promise() + + ctrl = $controller("WatchButton", $scope, { + item: {is_watcher: true} + onWatch: mocks.onWatch + onUnwatch: mocks.onUnwatch + }) + + promise = ctrl.toggleWatch() + + expect(ctrl.loading).to.be.true; + + mocks.onUnwatch.resolve() + + promise.finally () -> + expect(mocks.onUnwatch).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() diff --git a/app/modules/components/watch-button/watch-button.directive.coffee b/app/modules/components/watch-button/watch-button.directive.coffee new file mode 100644 index 00000000..2aad52b5 --- /dev/null +++ b/app/modules/components/watch-button/watch-button.directive.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-button.directive.coffee +### + +WatchButtonDirective = -> + return { + scope: {} + controller: "WatchButton", + bindToController: { + item: "=", + onWatch: "=", + onUnwatch: "=" + } + controllerAs: "vm", + templateUrl: "components/watch-button/watch-button.html", + } + +angular.module("taigaComponents").directive("tgWatchButton", WatchButtonDirective) diff --git a/app/modules/components/watch-button/watch-button.jade b/app/modules/components/watch-button/watch-button.jade new file mode 100644 index 00000000..2d25bbde --- /dev/null +++ b/app/modules/components/watch-button/watch-button.jade @@ -0,0 +1,44 @@ +mixin counter + span.track-button-counter( + title="{{ 'COMMON.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.watchers.length||0}:'messageformat' }}", + tg-loading="vm.loading" + ) + | {{ vm.item.watchers.length }} + + +//- Registered user button +a.track-button.watch-button.watch-container( + href="" + title="{{ 'COMMON.WATCH_BUTTON.BUTTON_TITLE' | translate }}" + ng-if="::vm.user" + ng-click="vm.toggleWatch()" + ng-class="{'active': vm.item.is_watcher, 'is-hover': vm.item.is_watcher && vm.isMouseOver}" + ng-mouseover="vm.showTextWhenMouseIsOver()" + ng-mouseleave="vm.showTextWhenMouseIsLeave()" +) + span.track-inner + span.track-icon + include ../../../svg/watch.svg + span( + ng-if="!vm.item.is_watcher", + translate="COMMON.WATCH_BUTTON.WATCH" + ) + span( + ng-if="vm.item.is_watcher && !vm.isMouseOver", + translate="COMMON.WATCH_BUTTON.WATCHING" + ) + span( + ng-if="vm.item.is_watcher && vm.isMouseOver", + translate="COMMON.WATCH_BUTTON.UNWATCH" + ) + +counter + +//- Anonymous user button +span.track-button.watch-button.watch-container( + ng-if="::!vm.user" +) + span.track-inner + span.track-icon + include ../../../svg/watch.svg + span(translate="COMMON.WATCH_BUTTON.WATCHERS") + +counter diff --git a/app/modules/external-apps/external-app.controller.coffee b/app/modules/external-apps/external-app.controller.coffee new file mode 100644 index 00000000..df0f0af7 --- /dev/null +++ b/app/modules/external-apps/external-app.controller.coffee @@ -0,0 +1,77 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: external-app.controller.coffee +### + +taiga = @.taiga + +class ExternalAppController extends taiga.Controller + @.$inject = [ + "$routeParams", + "tgExternalAppsService", + "$window", + "tgCurrentUserService", + "$location", + "$tgNavUrls", + "tgXhrErrorService", + "tgLoader" + ] + + constructor: (@routeParams, @externalAppsService, @window, @currentUserService, @location, + @navUrls, @xhrError, @loader) -> + @loader.start(false) + @._applicationId = @routeParams.application + @._state = @routeParams.state + @._getApplicationToken() + @._user = @currentUserService.getUser() + @._application = null + nextUrl = encodeURIComponent(@location.url()) + loginUrl = @navUrls.resolve("login") + @.loginWithAnotherUserUrl = "#{loginUrl}?next=#{nextUrl}" + + taiga.defineImmutableProperty @, "user", () => @._user + taiga.defineImmutableProperty @, "application", () => @._application + + _redirect: (applicationToken) => + nextUrl = applicationToken.get("next_url") + @window.open(nextUrl, "_self") + + _getApplicationToken: => + return @externalAppsService.getApplicationToken(@._applicationId, @._state) + .then (data) => + @._application = data.get("application") + if data.get("auth_code") + @._redirect(data) + else + @loader.pageLoaded() + + .catch (xhr) => + @loader.pageLoaded() + return @xhrError.response(xhr) + + cancel: () -> + @window.history.back() + + createApplicationToken: => + return @externalAppsService.authorizeApplicationToken(@._applicationId, @._state) + .then (data) => + @._redirect(data) + .catch (xhr) => + return @xhrError.response(xhr) + + +angular.module("taigaExternalApps").controller("ExternalApp", ExternalAppController) diff --git a/app/modules/external-apps/external-app.controller.spec.coffee b/app/modules/external-apps/external-app.controller.spec.coffee new file mode 100644 index 00000000..4d74e74e --- /dev/null +++ b/app/modules/external-apps/external-app.controller.spec.coffee @@ -0,0 +1,183 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: external-app.controller.spec.coffee +### + +describe "ExternalAppController", -> + provide = null + $controller = null + $rootScope = null + mocks = {} + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + _mockRouteParams = () -> + mocks.routeParams = {} + provide.value "$routeParams", mocks.routeParams + + _mockTgExternalAppsService = () -> + mocks.tgExternalAppsService = { + getApplicationToken: sinon.stub() + authorizeApplicationToken: sinon.stub() + } + provide.value "tgExternalAppsService", mocks.tgExternalAppsService + + _mockWindow = () -> + mocks.window = { + open: sinon.stub() + history: { + back: sinon.stub() + } + } + provide.value "$window", mocks.window + + _mockTgCurrentUserService = () -> + mocks.tgCurrentUserService = { + getUser: sinon.stub() + } + provide.value "tgCurrentUserService", mocks.tgCurrentUserService + + _mockLocation = () -> + mocks.location = { + url: sinon.stub() + } + provide.value "$location", mocks.location + + _mockTgNavUrls = () -> + mocks.tgNavUrls = { + resolve: sinon.stub() + } + provide.value "$tgNavUrls", mocks.tgNavUrls + + _mockTgXhrErrorService = () -> + mocks.tgXhrErrorService = { + response: sinon.spy(), + notFound: sinon.spy() + } + provide.value "tgXhrErrorService", mocks.tgXhrErrorService + + _mockTgLoader = () -> + mocks.tgLoader = { + start: sinon.stub(), + pageLoaded: sinon.stub() + } + provide.value "tgLoader", mocks.tgLoader + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockRouteParams() + _mockTgExternalAppsService() + _mockWindow() + _mockTgCurrentUserService() + _mockLocation() + _mockTgNavUrls() + _mockTgXhrErrorService() + _mockTgLoader() + return null + + beforeEach -> + module "taigaExternalApps" + _mocks() + _inject() + + it "not existing application", (done) -> + $scope = $rootScope.$new() + + mocks.routeParams.application = 6 + mocks.routeParams.state = "testing-state" + + xhr = { + status: 404 + } + + mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().reject(xhr) + + ctrl = $controller("ExternalApp") + + setTimeout ( -> + expect(mocks.tgLoader.start.withArgs(false)).to.be.calledOnce + expect(mocks.tgXhrErrorService.response.withArgs(xhr)).to.be.calledOnce + done() + ) + + it "existing application and existing token, automatically redirecting to next url", (done) -> + $scope = $rootScope.$new() + + mocks.routeParams.application = 6 + mocks.routeParams.state = "testing-state" + + applicationToken = Immutable.fromJS({ + auth_code: "testing-auth-code" + next_url: "http://next.url" + }) + + mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken) + + ctrl = $controller("ExternalApp") + + setTimeout ( -> + expect(mocks.tgLoader.start.withArgs(false)).to.be.calledOnce + expect(mocks.window.open.callCount).to.be.equal(1) + expect(mocks.window.open.calledWith("http://next.url")).to.be.true + done() + ) + + it "existing application and creating new token", (done) -> + $scope = $rootScope.$new() + + mocks.routeParams.application = 6 + mocks.routeParams.state = "testing-state" + + applicationToken = Immutable.fromJS({}) + mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken) + + ctrl = $controller("ExternalApp") + + applicationToken = Immutable.fromJS({ + next_url: "http://next.url" + auth_code: "testing-auth-code" + }) + + mocks.tgExternalAppsService.authorizeApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken) + + ctrl.createApplicationToken() + + setTimeout ( -> + expect(mocks.tgLoader.start.withArgs(false)).to.be.calledOnce + expect(mocks.tgLoader.pageLoaded).to.be.calledOnce + expect(mocks.window.open.callCount).to.be.equal(1) + expect(mocks.window.open.calledWith("http://next.url")).to.be.true + done() + ) + + it "cancel back to previous url", () -> + $scope = $rootScope.$new() + + mocks.routeParams.application = 6 + mocks.routeParams.state = "testing-state" + + applicationToken = Immutable.fromJS({}) + mocks.tgExternalAppsService.getApplicationToken.withArgs(mocks.routeParams.application, mocks.routeParams.state).promise().resolve(applicationToken) + + ctrl = $controller("ExternalApp") + expect(mocks.window.history.back.callCount).to.be.equal(0) + ctrl.cancel() + expect(mocks.window.history.back.callCount).to.be.equal(1) diff --git a/app/modules/external-apps/external-app.jade b/app/modules/external-apps/external-app.jade new file mode 100644 index 00000000..59d42918 --- /dev/null +++ b/app/modules/external-apps/external-app.jade @@ -0,0 +1,28 @@ +section.external-app-wrapper + div.logo + include ../../svg/logo-color.svg + + h1 Taiga + + h2(translate="EXTERNAL_APP.AUTHORIZATION_REQUEST", translate-values="{application: vm.application.get('name')}") + + div.user-card.avatar + .card-inner + div.user-image + img(ng-src="{{::vm.user.get('photo')}}", alt="{{::vm.user.get('full_name_display')}}") + div.user-data + h3 {{ ::vm.user.get("full_name_display") }} + p {{ ::vm.user.get("email") }} + a(ng-href="{{::vm.loginWithAnotherUserUrl}}", title="{{'EXTERNAL_APP.LOGIN_WITH_ANOTHER_USER' | translate}}", translate="EXTERNAL_APP.LOGIN_WITH_ANOTHER_USER") + + div.app-card + .card-inner + div.app-image + img(ng-src="{{::vm.application.get('icon_url')}}", alt="{{::vm.application.get('name')}}") + div.app-data + h3 {{ ::vm.application.get("name") }} + a(ng-href="{{::vm.application.get('web')}}", title="{{::vm.application.get('name')}}", target="_blank") {{ ::vm.application.get('web') }} + p {{ ::vm.application.get("description") }} + + a.button-green(href="#", ng-click="vm.createApplicationToken()", title="{{'EXTERNAL_APP.AUTHORIZE_APP' | translate}}", translate="EXTERNAL_APP.AUTHORIZE_APP") + a.cancel(href="#", ng-click="vm.cancel()", title="{{'EXTERNAL_APP.CANCEL' | translate}}", translate="EXTERNAL_APP.CANCEL") diff --git a/app/modules/external-apps/external-app.scss b/app/modules/external-apps/external-app.scss new file mode 100644 index 00000000..9a387368 --- /dev/null +++ b/app/modules/external-apps/external-app.scss @@ -0,0 +1,85 @@ +.external-app-wrapper { + margin: 2rem auto; + text-align: center; + width: 480px; + .logo { + height: 6rem; + margin: 0 auto; + width: 6rem; + } + h1 { + margin-bottom: 0; + } + .app-card, + .user-card { + line-height: 1.4; + margin-bottom: 2rem; + text-align: left; + .card-inner { + display: flex; + } + img { + width: 100%; + } + h3, + p { + margin: 0; + } + h3 { + @extend %large; + } + a { + @extend %xsmall; + display: block; + } + } + .app-card { + .app-image { + flex-basis: 100px; + margin-right: 1rem; + max-width: 105px; + } + .app-data { + flex: 1; + } + a { + margin-bottom: .5rem; + + } + p { + @extend %xsmall; + } + } + .user-card { + background: $card; + border: 1px solid $card-hover; + padding: 1rem; + .card-inner { + margin-bottom: .5rem; + } + .user-image { + flex-basis: 50px; + margin-right: 1rem; + max-width: 55px; + } + } + .button-green { + display: block; + } + .cancel { + @extend %small; + display: block; + margin-top: .5rem; + text-align: left; + } +} + +@include breakpoint(mobile) { + .external-app-wrapper { + margin: 0; + min-width: 100%; + padding: 2rem 1rem; + text-align: center; + width: 100%; + } +} diff --git a/app/modules/external-apps/external-app.service.coffee b/app/modules/external-apps/external-app.service.coffee new file mode 100644 index 00000000..f9aca954 --- /dev/null +++ b/app/modules/external-apps/external-app.service.coffee @@ -0,0 +1,33 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: external-app.service.coffee +### + +class ExternalAppsService extends taiga.Service + @.$inject = [ + "tgResources" + ] + + constructor: (@rs) -> + + getApplicationToken: (applicationId, state) -> + return @rs.externalapps.getApplicationToken(applicationId, state) + + authorizeApplicationToken: (applicationId, state) -> + return @rs.externalapps.authorizeApplicationToken(applicationId, state) + +angular.module("taigaExternalApps").service("tgExternalAppsService", ExternalAppsService) diff --git a/app/modules/external-apps/external-app.service.spec.coffee b/app/modules/external-apps/external-app.service.spec.coffee new file mode 100644 index 00000000..79dd8e98 --- /dev/null +++ b/app/modules/external-apps/external-app.service.spec.coffee @@ -0,0 +1,63 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: external-app.service.spec.coffee +### + +describe "tgExternalAppsService", -> + externalAppsService = provide = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + externalapps: { + getApplicationToken: sinon.stub() + authorizeApplicationToken: sinon.stub() + } + } + + provide.value "tgResources", mocks.tgResources + + _inject = (callback) -> + inject (_tgExternalAppsService_) -> + externalAppsService = _tgExternalAppsService_ + callback() if callback + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgResources() + return null + + _setup = -> + _mocks() + + beforeEach -> + module "taigaExternalApps" + _setup() + _inject() + + it "getApplicationToken", () -> + expect(mocks.tgResources.externalapps.getApplicationToken.callCount).to.be.equal(0) + externalAppsService.getApplicationToken(6, "testing-state") + expect(mocks.tgResources.externalapps.getApplicationToken.callCount).to.be.equal(1) + expect(mocks.tgResources.externalapps.getApplicationToken.calledWith(6, "testing-state")).to.be.true + + it "authorizeApplicationToken", () -> + expect(mocks.tgResources.externalapps.authorizeApplicationToken.callCount).to.be.equal(0) + externalAppsService.authorizeApplicationToken(6, "testing-state") + expect(mocks.tgResources.externalapps.authorizeApplicationToken.callCount).to.be.equal(1) + expect(mocks.tgResources.externalapps.authorizeApplicationToken.calledWith(6, "testing-state")).to.be.true diff --git a/app/modules/external-apps/external-apps.module.coffee b/app/modules/external-apps/external-apps.module.coffee new file mode 100644 index 00000000..97e0f201 --- /dev/null +++ b/app/modules/external-apps/external-apps.module.coffee @@ -0,0 +1,20 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: external-apps.module.coffee +### + +module = angular.module("taigaExternalApps", []) diff --git a/app/modules/feedback/feedback.service.coffee b/app/modules/feedback/feedback.service.coffee index 06f3e5f5..fe3293d5 100644 --- a/app/modules/feedback/feedback.service.coffee +++ b/app/modules/feedback/feedback.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: feedback.service.coffee +### + class FeedbackService extends taiga.Service @.$inject = ["tgLightboxFactory"] diff --git a/app/modules/feedback/feedback.service.spec.coffee b/app/modules/feedback/feedback.service.spec.coffee index da067961..1c6927e6 100644 --- a/app/modules/feedback/feedback.service.spec.coffee +++ b/app/modules/feedback/feedback.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: feedback.service.spec.coffee +### + describe "tgFeedbackService", -> feedbackService = provide = null mocks = {} @@ -35,4 +54,4 @@ describe "tgFeedbackService", -> params = { "class": "lightbox lightbox-feedback lightbox-generic-form" } - expect(mocks.tgLightboxFactory.create.calledWith("tg-lb-feedback", params)).to.be.true() + expect(mocks.tgLightboxFactory.create.calledWith("tg-lb-feedback", params)).to.be.true diff --git a/app/modules/home/duties/duty.directive.coffee b/app/modules/home/duties/duty.directive.coffee index d6eae86f..6482d460 100644 --- a/app/modules/home/duties/duty.directive.coffee +++ b/app/modules/home/duties/duty.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: duty.directive.coffee +### + DutyDirective = (navurls, $translate) -> link = (scope, el, attrs, ctrl) -> scope.vm = {} diff --git a/app/modules/home/duties/duty.directive.spec.coffee b/app/modules/home/duties/duty.directive.spec.coffee index 0cc57e75..bf631820 100644 --- a/app/modules/home/duties/duty.directive.spec.coffee +++ b/app/modules/home/duties/duty.directive.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: duty.directive.spec.coffee +### + describe "dutyDirective", () -> scope = compile = provide = null mockTgProjectsService = null diff --git a/app/modules/home/duties/duty.jade b/app/modules/home/duties/duty.jade index c1877394..d3efacee 100644 --- a/app/modules/home/duties/duty.jade +++ b/app/modules/home/duties/duty.jade @@ -1,18 +1,23 @@ -a(href="{{ ::vm.duty.get('url') }}", title="{{ ::duty.get('subject') }}") - - img.avatar(ng-if="::vm.duty.get('assigned_to_extra_info')" - ng-src="{{ ::vm.duty.get('assigned_to_extra_info').get('photo') }}" - title="{{ ::vm.duty.get('assigned_to_extra_info').get('full_name_display') }}") - - img.avatar(ng-if="::!vm.duty.get('assigned_to_extra_info')" - src="/images/unnamed.png" - title="{{'ACTIVITY.VALUES.UNASSIGNED' | translate}}") - - div.duty-data - div - span.duty-type {{ ::vm.getDutyType() }} - span.duty-status(ng-style="{'color': vm.duty.get('status_extra_info').get('color')}") {{ ::vm.duty.get('status_extra_info').get('name') }} - span.duty-title - span.duty-id(tg-bo-ref="duty.get('ref')") - span.duty-name {{ ::duty.get('subject') }} - div.duty-project {{ ::vm.duty.get('projectName')}} +a.list-itemtype-ticket( + href="{{ ::vm.duty.get('url') }}" + title="{{ ::duty.get('subject') }}" +) + div.list-itemtype-avatar(ng-if="::vm.duty.get('assigned_to_extra_info')") + img( + ng-src="{{ ::vm.duty.get('assigned_to_extra_info').get('photo') }}" + title="{{ ::vm.duty.get('assigned_to_extra_info').get('full_name_display') }}" + ) + div.list-itemtype-avatar(ng-if="::!vm.duty.get('assigned_to_extra_info')") + img( + src="/images/unnamed.png" + title="{{'ACTIVITY.VALUES.UNASSIGNED' | translate}}" + ) + div.list-itemtype-ticket-data + p + span.ticket-project {{ ::vm.duty.get('projectName')}} + span.ticket-type {{ ::vm.getDutyType() }} + span.ticket-status(ng-style="{'color': vm.duty.get('status_extra_info').get('color')}") {{ ::vm.duty.get('status_extra_info').get('name') }} + + h2 + span.ticket-id(tg-bo-ref="duty.get('ref')") + span.ticket-title {{ ::duty.get('subject') }} diff --git a/app/modules/home/duties/duty.scss b/app/modules/home/duties/duty.scss index c4a2804b..b8ca2f03 100644 --- a/app/modules/home/duties/duty.scss +++ b/app/modules/home/duties/duty.scss @@ -4,72 +4,27 @@ .duty-single { border-bottom: 1px solid $whitish; cursor: pointer; - padding: .5rem; + transition: background .2s; + transition-delay: .2s; + &:hover { + background: rgba($primary-light, .1); + } &:last-child { border: 0; } &.blocked { background: rgba($red-light, .2); - .duty-type, - .duty-status { - color: $red; - } + color: $red; } >a { align-items: center; + border-bottom: 0; display: flex; flex-direction: row; } } - .avatar { - flex-basis: 47px; - height: 47px; - margin-right: .5rem; - width: 47px; - } - .duty-data { - flex: 1; - margin-right: .5rem; - } - .duty-type, - .duty-status { - @extend %small; - color: $gray; - margin-right: .3rem; - } - .duty-title { - display: block; - margin-top: .25rem; - } - .duty-id { - color: $gray-light; - margin-right: .3rem; - } - .duty-project { - @extend %small; - align-self: flex-start; - color: $gray-light; - margin-left: auto; - text-align: right; - width: 120px; - } .see-more { display: block; margin: 2rem 30%; } } -.watching-empty { - padding: 5vh; - text-align: center; - svg { - margin: 2rem auto; - max-width: 160px; - text-align: center; - path { - fill: $whitish; - } - } - p { - @extend %small; - } -} diff --git a/app/modules/home/home.jade b/app/modules/home/home.jade index eb815985..d1af66bd 100644 --- a/app/modules/home/home.jade +++ b/app/modules/home/home.jade @@ -1,6 +1,5 @@ doctype html -include ../../partials/includes/components/beta div.home-wrapper.centered div.duty-summary div(tg-working-on) diff --git a/app/modules/home/home.module.coffee b/app/modules/home/home.module.coffee index 9cdb1f39..6893154e 100644 --- a/app/modules/home/home.module.coffee +++ b/app/modules/home/home.module.coffee @@ -1 +1,20 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: home.module.coffee +### + module = angular.module("taigaHome", []) diff --git a/app/modules/home/home.scss b/app/modules/home/home.scss index 769c74ca..e605acea 100644 --- a/app/modules/home/home.scss +++ b/app/modules/home/home.scss @@ -1,6 +1,5 @@ .home-wrapper { display: flex; - padding-top: 2rem; .duty-summary { flex: 1; margin-right: 2rem; diff --git a/app/modules/home/home.service.coffee b/app/modules/home/home.service.coffee index 054ddea5..46cc0ea5 100644 --- a/app/modules/home/home.service.coffee +++ b/app/modules/home/home.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: home.service.coffee +### + groupBy = @.taiga.groupBy class HomeService extends taiga.Service diff --git a/app/modules/home/home.service.spec.coffee b/app/modules/home/home.service.spec.coffee index 776cac40..791a82be 100644 --- a/app/modules/home/home.service.spec.coffee +++ b/app/modules/home/home.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: home.service.spec.coffee +### + describe "tgHome", -> homeService = provide = null mocks = {} diff --git a/app/modules/home/projects/home-project-list-directive.spec.coffee b/app/modules/home/projects/home-project-list-directive.spec.coffee index eccd78ec..a6f8bd5d 100644 --- a/app/modules/home/projects/home-project-list-directive.spec.coffee +++ b/app/modules/home/projects/home-project-list-directive.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: home-project-list-directive.spec.coffee +### + describe "homeProjectListDirective", () -> scope = compile = provide = null mocks = {} diff --git a/app/modules/home/projects/home-project-list.directive.coffee b/app/modules/home/projects/home-project-list.directive.coffee index 4af582e0..be30e9a8 100644 --- a/app/modules/home/projects/home-project-list.directive.coffee +++ b/app/modules/home/projects/home-project-list.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: home-project-list.directive.coffee +### + HomeProjectListDirective = (currentUserService, projectsService) -> link = (scope, el, attrs, ctrl) -> scope.vm = {} diff --git a/app/modules/home/projects/home-project-list.jade b/app/modules/home/projects/home-project-list.jade index c273ab2a..54040b22 100644 --- a/app/modules/home/projects/home-project-list.jade +++ b/app/modules/home/projects/home-project-list.jade @@ -1,20 +1,23 @@ -ul.home-project-list(ng-show="vm.projects.size") - 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") ... +section.home-project-list(ng-show="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") ... -a.see-more-projects-btn.button-gray(href="#", - ng-show="vm.projects.size", - tg-nav="projects", - title="{{'PROJECT.NAVIGATION.SEE_MORE_PROJECTS' | translate}}", - translate="PROJECT.NAVIGATION.SEE_MORE_PROJECTS") + a.see-more-projects-btn.button-gray( + href="#", + ng-show="vm.projects.size", + tg-nav="projects", + title="{{'PROJECT.NAVIGATION.SEE_MORE_PROJECTS' | translate}}", + translate="PROJECT.NAVIGATION.SEE_MORE_PROJECTS" + ) -section.projects-empty(ng-hide="vm.projects.size") +section.projects-empty(ng-show="!vm.projects.size") include ../../../svg/empty-project.svg p(translate="HOME.EMPTY_PROJECT_LIST") a.create-project-button.button-green(href="#", ng-click="vm.newProject()", diff --git a/app/modules/home/projects/home-project-list.scss b/app/modules/home/projects/home-project-list.scss index 3b322609..21d67b31 100644 --- a/app/modules/home/projects/home-project-list.scss +++ b/app/modules/home/projects/home-project-list.scss @@ -7,7 +7,7 @@ padding: 1rem; text-overflow: ellipsis; &:hover { - border-color: $fresh-taiga; + border-color: $primary-light; transition: all .3s linear; p { color: $gray; @@ -18,11 +18,11 @@ transition: fill .3s linear; } } - } - a { - display: flex; - flex-direction: column; - min-height: 5rem; + a { + display: flex; + flex-direction: column; + min-height: 5rem; + } } h2 { @extend %text; @@ -63,6 +63,7 @@ } p { @extend %small; + @extend %light; } .create-project-button { display: block; diff --git a/app/modules/home/working-on/empty.jade b/app/modules/home/working-on/empty.jade new file mode 100644 index 00000000..c9193b9a --- /dev/null +++ b/app/modules/home/working-on/empty.jade @@ -0,0 +1,6 @@ +- for (var x = 0; x < 2; x++) + .empty-ticket + .avatar + .data + .line + .line diff --git a/app/modules/home/working-on/empty.scss b/app/modules/home/working-on/empty.scss new file mode 100644 index 00000000..cf8c5c3c --- /dev/null +++ b/app/modules/home/working-on/empty.scss @@ -0,0 +1,43 @@ +.working-on-empty, +.watching-empty { + margin-bottom: 4rem; + p { + @extend %light; + margin: 2rem 9rem 1rem; + text-align: center; + } +} + +.empty-ticket { + display: flex; + &:not(:last-child) { + border-bottom: 1px solid $whitish; + padding: 1rem 0; + } + &:last-child { + padding: 1rem 0 0; + } + .avatar { + background: darken($whitish, 5%); + flex-basis: 48px; + height: 48px; + margin-right: 1rem; + width: 48px; + } + + .data { + display: flex; + flex-direction: column; + } + + .line { + background: $whitish; + height: 1rem; + margin-bottom: 1rem; + width: 40vw; + &:last-child { + margin: 0; + width: 20vw; + } + } +} diff --git a/app/modules/home/working-on/working-on.controller.coffee b/app/modules/home/working-on/working-on.controller.coffee index 16f763d1..7f7dd05b 100644 --- a/app/modules/home/working-on/working-on.controller.coffee +++ b/app/modules/home/working-on/working-on.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: working-on.controller.coffee +### + class WorkingOnController @.$inject = [ "tgHomeService" diff --git a/app/modules/home/working-on/working-on.controller.spec.coffee b/app/modules/home/working-on/working-on.controller.spec.coffee index 7d19a00e..ed9f02d7 100644 --- a/app/modules/home/working-on/working-on.controller.spec.coffee +++ b/app/modules/home/working-on/working-on.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: working-on.controller.spec.coffee +### + describe "WorkingOn", -> $controller = null $provide = null diff --git a/app/modules/home/working-on/working-on.directive.coffee b/app/modules/home/working-on/working-on.directive.coffee index 3e0cdaae..10ec4899 100644 --- a/app/modules/home/working-on/working-on.directive.coffee +++ b/app/modules/home/working-on/working-on.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: working-on.directive.coffee +### + WorkingOnDirective = (homeService, currentUserService) -> link = (scope, el, attrs, ctrl) -> user = currentUserService.getUser() diff --git a/app/modules/home/working-on/working-on.jade b/app/modules/home/working-on/working-on.jade index 34e287ef..f12edae1 100644 --- a/app/modules/home/working-on/working-on.jade +++ b/app/modules/home/working-on/working-on.jade @@ -1,12 +1,19 @@ -div.title-bar.working-on-title(ng-show="vm.assignedTo.size", translate="HOME.WORKING_ON_SECTION") -section.working-on(ng-show="vm.assignedTo.size") - div.duty-single(tg-duty="duty", tg-repeat="duty in vm.assignedTo", ng-class="{blocked: duty.is_blocked}") +section.working-on-container + .title-bar.working-on-title(translate="HOME.WORKING_ON_SECTION") -div.title-bar.watching-title(translate="HOME.WATCHING_SECTION") + .working-on(ng-show="vm.assignedTo.size") + .duty-single(tg-duty="duty", tg-repeat="duty in vm.assignedTo", ng-class="{blocked: duty.is_blocked}") -section.watching-empty(ng-show="!vm.watching.size") - include ../../../svg/hide.svg - p(translate="HOME.EMPTY_WATCHING") + .working-on-empty(ng-show="!vm.assignedTo.size") + p(translate="HOME.EMPTY_WORKING_ON") + include empty.jade -section.watching(ng-show="vm.watching.size") - div.duty-single(tg-duty="duty", tg-repeat="duty in vm.watching", ng-class="{blocked: duty.is_blocked}") \ No newline at end of file +section.watching-container + .title-bar.watching-title(translate="HOME.WATCHING_SECTION") + + .watching(ng-show="vm.watching.size") + .duty-single(tg-duty="duty", tg-repeat="duty in vm.watching", ng-class="{blocked: duty.is_blocked}") + + .watching-empty(ng-show="!vm.watching.size") + p(translate="HOME.EMPTY_WATCHING") + include empty.jade diff --git a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.coffee b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.coffee index b125c86c..d90d3f36 100644 --- a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.coffee +++ b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: dropdown-project-list.directive.coffee +### + DropdownProjectListDirective = (currentUserService, projectsService) -> link = (scope, el, attrs, ctrl) -> scope.vm = {} diff --git a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.spec.coffee b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.spec.coffee index 4f144067..263b7f58 100644 --- a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.spec.coffee +++ b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.directive.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: dropdown-project-list.directive.spec.coffee +### + describe "dropdownProjectListDirective", () -> scope = compile = provide = null mocks = {} diff --git a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade index 37b5800d..234a4ba2 100644 --- a/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade +++ b/app/modules/navigation-bar/dropdown-project-list/dropdown-project-list.jade @@ -3,9 +3,8 @@ a(href="", title="Projects", tg-nav="projects") div.navbar-dropdown.dropdown-project-list ul - a(href="#", - tg-repeat="project in vm.projects track by project.get('id')", - tg-nav="project:project=project.get('slug')") {{::project.get("name")}} + li(tg-repeat="project in vm.projects track by project.get('id')") + a(href="#", tg-nav="project:project=project.get('slug')") {{::project.get("name")}} a.see-more-projects-btn.button-gray( href="#", diff --git a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee index fd9e69d6..3c6f9385 100644 --- a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee +++ b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: dropdown-user.directive.coffee +### + DropdownUserDirective = (authService, configService, locationService, navUrlsService, feedbackService) -> diff --git a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee index b8eeb527..62ce337f 100644 --- a/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee +++ b/app/modules/navigation-bar/dropdown-user/dropdown-user.directive.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: dropdown-user.directive.spec.coffee +### + describe "dropdownUserDirective", () -> scope = compile = provide = null mockTgAuth = null @@ -87,7 +106,7 @@ describe "dropdownUserDirective", () -> 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.path.calledWith("/login")).to.be.true it "dropdown user send feedback", () -> elm = createDirective() diff --git a/app/modules/navigation-bar/dropdown-user/dropdown-user.jade b/app/modules/navigation-bar/dropdown-user/dropdown-user.jade index 2eab692b..7d3d275e 100644 --- a/app/modules/navigation-bar/dropdown-user/dropdown-user.jade +++ b/app/modules/navigation-bar/dropdown-user/dropdown-user.jade @@ -1,5 +1,13 @@ -a.user-avatar(tg-nav="profile", title="{{ vm.user.get('full_name_display') }}") {{ vm.user.get('full_name_display') }} - img(ng-src="{{ vm.user.get('photo') }}", alt="{{ vm.user.get('full_name_display') }}") +a.user-avatar( + tg-nav="profile" + title="{{ vm.user.get('full_name_display') }}" + ) {{ vm.user.get('full_name_display') }} + img( + ng-src="{{ vm.user.get('photo') }}" + alt="{{ vm.user.get('full_name_display') }}" + width="48px" + height="40px" + ) div.navbar-dropdown.dropdown-user ul @@ -21,17 +29,6 @@ div.navbar-dropdown.dropdown-user title="{{'PROJECT.NAVIGATION.CHANGE_PASSWORD_TITLE' | translate}}", translate="PROJECT.NAVIGATION.CHANGE_PASSWORD") - //li - // a( - // href="#", - // title="{{'PROJECT.NAVIGATION.ORGANIZATIONS_TITLE' | translate}}", - // translate="PROJECT.NAVIGATION.ORGANIZATIONS") - //li - // a( - // href="#", - // title="{{'PROJECT.NAVIGATION.SETTINGS_TITLE' | translate}}", - // translate="PROJECT.NAVIGATION.SETTINGS") - li a( href="#", diff --git a/app/modules/navigation-bar/navigation-bar.directive.coffee b/app/modules/navigation-bar/navigation-bar.directive.coffee index e4da024d..8404cc91 100644 --- a/app/modules/navigation-bar/navigation-bar.directive.coffee +++ b/app/modules/navigation-bar/navigation-bar.directive.coffee @@ -1,4 +1,23 @@ -NavigationBarDirective = (currentUserService, $location) -> +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: navigation-bar.directive.coffee +### + +NavigationBarDirective = (currentUserService, navigationBarService, $location) -> link = (scope, el, attrs, ctrl) -> scope.vm = {} @@ -10,6 +29,8 @@ NavigationBarDirective = (currentUserService, $location) -> taiga.defineImmutableProperty(scope.vm, "projects", () -> currentUserService.projects.get("recents")) taiga.defineImmutableProperty(scope.vm, "isAuthenticated", () -> currentUserService.isAuthenticated()) + taiga.defineImmutableProperty(scope.vm, "isEnabledHeader", () -> navigationBarService.isEnabledHeader()) + directive = { templateUrl: "navigation-bar/navigation-bar.html" @@ -21,6 +42,7 @@ NavigationBarDirective = (currentUserService, $location) -> NavigationBarDirective.$inject = [ "tgCurrentUserService", + "tgNavigationBarService" "$location" ] diff --git a/app/modules/navigation-bar/navigation-bar.directive.spec.coffee b/app/modules/navigation-bar/navigation-bar.directive.spec.coffee index ffb906ee..9195b70a 100644 --- a/app/modules/navigation-bar/navigation-bar.directive.spec.coffee +++ b/app/modules/navigation-bar/navigation-bar.directive.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: navigation-bar.directive.spec.coffee +### + describe "navigationBarDirective", () -> scope = compile = provide = null mocks = {} diff --git a/app/modules/navigation-bar/navigation-bar.jade b/app/modules/navigation-bar/navigation-bar.jade index e1b842c9..f8202abf 100644 --- a/app/modules/navigation-bar/navigation-bar.jade +++ b/app/modules/navigation-bar/navigation-bar.jade @@ -1,4 +1,4 @@ -nav.navbar +nav.navbar(ng-if="vm.isEnabledHeader") div.nav-left a.logo( href="#", diff --git a/app/modules/navigation-bar/navigation-bar.module.coffee b/app/modules/navigation-bar/navigation-bar.module.coffee index a6270366..b1acfa2d 100644 --- a/app/modules/navigation-bar/navigation-bar.module.coffee +++ b/app/modules/navigation-bar/navigation-bar.module.coffee @@ -1 +1,20 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: navigation-bar.module.coffee +### + angular.module("taigaNavigationBar", []) diff --git a/app/modules/navigation-bar/navigation-bar.scss b/app/modules/navigation-bar/navigation-bar.scss index c00e7f68..920a1eea 100644 --- a/app/modules/navigation-bar/navigation-bar.scss +++ b/app/modules/navigation-bar/navigation-bar.scss @@ -1,11 +1,11 @@ +$dropdown-width: 350px; + .navbar { - background: rgba($black, .5); display: flex; height: $navbar; justify-content: space-between; position: relative; &:after { - background: url('../images/menu-vert.png') repeat top left; background-size: 200%; bottom: 0; content: ''; @@ -24,6 +24,7 @@ } .nav-left { >a { + color: $white; padding: .5rem 1.5rem; &.logo { background: rgba($black, .2); @@ -41,6 +42,7 @@ .nav-right { margin-left: auto; a { + color: $white; padding: .5rem 2rem; } } @@ -48,10 +50,12 @@ color: $white; display: inline-block; transition: all .2s linear; - &.active, + svg path { + fill: darken($primary-dark, 8%); + } &:hover { background: rgba($black, .2); - color: $fresh-taiga; + color: $primary-light; svg path { fill: $white; } @@ -77,7 +81,7 @@ height: 1.2rem; max-width: 1.2rem; path { - fill: $dark-taiga; + fill: $top-icon-color; transition: all .2s; } } @@ -102,7 +106,7 @@ min-width: $dropdown-width; position: absolute; top: 2.4rem; - z-index: 99; + z-index: 999; } } @@ -130,7 +134,7 @@ padding: .8rem .5rem; &:hover { background: rgba($white, .1); - color: $fresh-taiga; + color: $primary-light; } &.see-more-projects-btn, &.create-organization-btn, diff --git a/app/modules/navigation-bar/navigation-bar.service.coffee b/app/modules/navigation-bar/navigation-bar.service.coffee new file mode 100644 index 00000000..dafdfaa0 --- /dev/null +++ b/app/modules/navigation-bar/navigation-bar.service.coffee @@ -0,0 +1,34 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: navigation-bar.service.coffee +### + +class NavigationBarService extends taiga.Service + + constructor: -> + @.disableHeader() + + enableHeader: -> + @.enabledHeader = true + + disableHeader: -> + @.enabledHeader = false + + isEnabledHeader: -> + return @.enabledHeader + +angular.module("taigaNavigationBar").service("tgNavigationBarService", NavigationBarService) diff --git a/app/modules/profile/includes/profile-favorites.jade b/app/modules/profile/includes/profile-favorites.jade deleted file mode 100644 index f0a723c7..00000000 --- a/app/modules/profile/includes/profile-favorites.jade +++ /dev/null @@ -1,6 +0,0 @@ -section.profile-favorites - nav.profile-favorites-filters - a.active(href="", title="No Filter") all - a(href="", title="Only show your team") projects - a(href="", title="Only show people you follow") US - a(href="", title="Only show people follow you") tasks diff --git a/app/modules/profile/profile-bar/profile-bar.controller.coffee b/app/modules/profile/profile-bar/profile-bar.controller.coffee index 902679ec..3c59817e 100644 --- a/app/modules/profile/profile-bar/profile-bar.controller.coffee +++ b/app/modules/profile/profile-bar/profile-bar.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-bar.controller.coffee +### + class ProfileBarController @.$inject = [ "tgUserService" diff --git a/app/modules/profile/profile-bar/profile-bar.controller.spec.coffee b/app/modules/profile/profile-bar/profile-bar.controller.spec.coffee index abf2a69e..ca0c33c1 100644 --- a/app/modules/profile/profile-bar/profile-bar.controller.spec.coffee +++ b/app/modules/profile/profile-bar/profile-bar.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-bar.controller.spec.coffee +### + describe "ProfileBar", -> $controller = null provide = null diff --git a/app/modules/profile/profile-bar/profile-bar.directive.coffee b/app/modules/profile/profile-bar/profile-bar.directive.coffee index 24744b44..cc3ff48c 100644 --- a/app/modules/profile/profile-bar/profile-bar.directive.coffee +++ b/app/modules/profile/profile-bar/profile-bar.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-bar.directive.coffee +### + ProfileBarDirective = () -> return { templateUrl: "profile/profile-bar/profile-bar.html", diff --git a/app/modules/profile/profile-bar/profile-bar.jade b/app/modules/profile/profile-bar/profile-bar.jade index a0c0017d..3c16c794 100644 --- a/app/modules/profile/profile-bar/profile-bar.jade +++ b/app/modules/profile/profile-bar/profile-bar.jade @@ -6,6 +6,7 @@ section.profile-bar // span(translate="USER.PROFILE.FOLLOW") div.profile-data h1(ng-class="{'not-full-name': !vm.user.get('full_name')}") {{::vm.user.get("full_name_display")}} + .username @{{::vm.user.get("username")}} h2 {{::vm.stats.get('roles').join(", ")}} // div.location // include ../../../svg/location.svg diff --git a/app/modules/profile/profile-contacts/profile-contacts.controller.coffee b/app/modules/profile/profile-contacts/profile-contacts.controller.coffee index b06f4768..f675e5f7 100644 --- a/app/modules/profile/profile-contacts/profile-contacts.controller.coffee +++ b/app/modules/profile/profile-contacts/profile-contacts.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-contacts.controller.coffee +### + class ProfileContactsController @.$inject = [ "tgUserService", diff --git a/app/modules/profile/profile-contacts/profile-contacts.controller.spec.coffee b/app/modules/profile/profile-contacts/profile-contacts.controller.spec.coffee index cf72cda4..0c060e78 100644 --- a/app/modules/profile/profile-contacts/profile-contacts.controller.spec.coffee +++ b/app/modules/profile/profile-contacts/profile-contacts.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-contacts.controller.spec.coffee +### + describe "ProfileContacts", -> $controller = null provide = null diff --git a/app/modules/profile/profile-contacts/profile-contacts.directive.coffee b/app/modules/profile/profile-contacts/profile-contacts.directive.coffee index 994a275b..252a9e75 100644 --- a/app/modules/profile/profile-contacts/profile-contacts.directive.coffee +++ b/app/modules/profile/profile-contacts/profile-contacts.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-contacts.directive.coffee +### + ProfileContactsDirective = () -> link = (scope, elm, attrs, ctrl) -> ctrl.loadContacts() diff --git a/app/modules/profile/profile-contacts/profile-contacts.jade b/app/modules/profile/profile-contacts/profile-contacts.jade index 265c26d7..49a79f62 100644 --- a/app/modules/profile/profile-contacts/profile-contacts.jade +++ b/app/modules/profile/profile-contacts/profile-contacts.jade @@ -11,32 +11,20 @@ section.profile-contacts div(ng-if="vm.isCurrentUser") p(translate="USER.PROFILE.CURRENT_USER_CONTACTS_EMPTY") p(translate="USER.PROFILE.CURRENT_USER_CONTACTS_EMPTY_EXPLAIN") + //- + nav.profile-contact-filters + a.active(href="", title="No Filter") all + a(href="", title="Only show your team") team + a(href="", title="Only show people you follow") following + a(href="", title="Only show people follow you") followers - // nav.profile-contact-filters - // a.active(href="", title="No Filter") all - // a(href="", title="Only show your team") team - // a(href="", title="Only show people you follow") following - // a(href="", title="Only show people follow you") followers + div.list-itemtype-user(tg-repeat="contact in ::vm.contacts") + a.list-itemtype-avatar(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('name')}}") + img(ng-src="{{::contact.get('photo')}}", alt="{{::contact.get('full_name')}}") - div.profile-contact-single(tg-repeat="contact in ::vm.contacts") - div.profile-contact-picture - a(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('name') }}") - img(ng-src="{{::contact.get('photo')}}", alt="{{::contact.get('full_name')}}") + div.list-itemtype-user-data + h2 + a(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('full_name_display') }}") {{::contact.get('full_name_display')}} - div.profile-contact-data - h1 - a(tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('full_name_display') }}") - | {{::contact.get('full_name_display')}} - - p(ng-if="contact.bio") {{::contact.get('bio')}} - - div.extra-info - span.position {{::contact.get('roles').join(", ")}} - // span.location todo - // div.profile-project-stats - // div.stat-projects(title="2 projects") - // span.icon.icon-project - // span.stat-num 2 - // div.stat-viewer(title="2 followers") - // span.icon.icon-open-eye - // span.stat-num 4 + p {{::contact.get('roles').join(", ")}} + p.extra-info(ng-if="contact.get('bio')") {{::contact.get('bio')}} diff --git a/app/modules/profile/profile-favs/items/items.directive.coffee b/app/modules/profile/profile-favs/items/items.directive.coffee new file mode 100644 index 00000000..626ab237 --- /dev/null +++ b/app/modules/profile/profile-favs/items/items.directive.coffee @@ -0,0 +1,39 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: items.directive.coffee +### + +FavItemDirective = -> + link = (scope, el, attrs, ctrl) -> + scope.vm = {item: scope.item} + + templateUrl = (el, attrs) -> + if attrs.itemType == "project" + return "profile/profile-favs/items/project.html" + else # if attr.itemType in ["userstory", "task", "issue"] + return "profile/profile-favs/items/ticket.html" + + return { + scope: { + "item": "=tgFavItem" + } + link: link + templateUrl: templateUrl + } + + +angular.module("taigaProfile").directive("tgFavItem", FavItemDirective) diff --git a/app/modules/profile/profile-favs/items/project.jade b/app/modules/profile/profile-favs/items/project.jade new file mode 100644 index 00000000..2cb121d9 --- /dev/null +++ b/app/modules/profile/profile-favs/items/project.jade @@ -0,0 +1,34 @@ +.list-itemtype-project + .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( + tg-repeat="tag in ::vm.item.get('tags_colors')" + style='border-left: 5px solid {{ ::tag.get("color") }};' + ) + span.tag-name {{ ::tag.get('name') }} + + .list-itemtype-track + span.list-itemtype-track-likers( + ng-class="{'active': vm.item.get('is_fan')}" + title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_fans\")||0}:'messageformat' }}" + ) + span.icon + include ../../../../svg/like.svg + span {{ ::vm.item.get('total_fans') }} + + span.list-itemtype-track-watchers( + ng-class="{'active': vm.item.get('is_watcher')}" + title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_watchers\")||0}:'messageformat' }}" + ) + span.icon + include ../../../../svg/watch.svg + span {{ ::vm.item.get('total_watchers') }} diff --git a/app/modules/profile/profile-favs/items/ticket.jade b/app/modules/profile/profile-favs/items/ticket.jade new file mode 100644 index 00000000..f8051bc4 --- /dev/null +++ b/app/modules/profile/profile-favs/items/ticket.jade @@ -0,0 +1,80 @@ +div.list-itemtype-ticket + a.list-itemtype-avatar( + href="" + ng-if="::vm.item.get('assigned_to')" + tg-nav="user-profile:username=vm.item.get('assigned_to_username')" + title="{{ ::vm.item.get('assigned_to_full_name') }}" + ) + img( + ng-src="{{ ::vm.item.get('assigned_to_photo') }}", + alt="{{ ::vm.item.get('assigned_to_full_name') }}" + ) + + a.list-itemtype-avatar( + href="" + ng-if="::!vm.item.get('assigned_to')", + title="{{ 'COMMON.ASSIGNED_TO.NOT_ASSIGNED'|translate }}" + ) + img( + src="/images/unnamed.png", + alt="{{ 'COMMON.ASSIGNED_TO.NOT_ASSIGNED'|translate }}" + ) + + div.list-itemtype-ticket-data + p + span.ticket-project + | {{:: vm.item.get('project_name') }} + span.ticket-type( + ng-if="::vm.item.get('type') === 'userstory'" + translate="COMMON.USER_STORY" + ) + span.ticket-type( + ng-if="::vm.item.get('type') === 'task'" + translate="COMMON.TASK" + ) + span.ticket-type( + ng-if="::vm.item.get('type') === 'issue'" + translate="COMMON.ISSUE" + ) + span.ticket-status(ng-style="::{'color': vm.item.get('status_color')}") + | {{:: vm.item.get('status') }} + h2 + span.ticket-id(tg-bo-ref="vm.item.get('ref')") + a.ticket-title( + href="#" + ng-if="::vm.item.get('type') === 'userstory'" + tg-nav="project-userstories-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" + title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" + ) + | {{ ::vm.item.get('subject') }} + a.ticket-title( + href="#" + ng-if="::vm.item.get('type') === 'task'" + tg-nav="project-tasks-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" + title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" + ) + | {{ ::vm.item.get('subject') }} + a.ticket-title( + href="#" + ng-if="::vm.item.get('type') === 'issue'" + tg-nav="project-issues-detail:project=vm.item.get('project_slug'),ref=vm.item.get('ref')" + title="#{{ ::vm.item.get('ref') }} {{ ::vm.item.get('subject') }}" + ) + | {{ ::vm.item.get('subject') }} + + div.list-itemtype-track + span.list-itemtype-track-likers( + ng-class="{'active': vm.item.get('is_voter')}", + title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_voters\")||0}:'messageformat' }}" + ) + span.icon + include ../../../../svg/upvote.svg + span {{ ::vm.item.get('total_voters') }} + + span.list-itemtype-track-watchers( + ng-class="{'active': vm.item.get('is_watcher')}" + title="{{ 'COMMON.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.item.get(\"total_watchers\")||0}:'messageformat' }}" + ) + span.icon + include ../../../../svg/watch.svg + span {{ ::vm.item.get('total_watchers') }} diff --git a/app/modules/profile/profile-favs/profile-favs.controller.coffee b/app/modules/profile/profile-favs/profile-favs.controller.coffee new file mode 100644 index 00000000..2fe034f2 --- /dev/null +++ b/app/modules/profile/profile-favs/profile-favs.controller.coffee @@ -0,0 +1,187 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-favs.controller.coffee +### + +debounceLeading = @.taiga.debounceLeading + +class FavsBaseController + constructor: -> + @._init() + + #@._getItems = null # Define in inheritance classes + # + _init: -> + @.enableFilterByAll = true + @.enableFilterByProjects = true + @.enableFilterByUserStories = true + @.enableFilterByTasks = true + @.enableFilterByIssues = true + @.enableFilterByTextQuery = true + + @._resetList() + @.q = null + @.type = null + + _resetList: -> + @.items = Immutable.List() + @.scrollDisabled = false + @._page = 1 + + _enableLoadingSpinner: -> + @.isLoading = true + + _disableLoadingSpinner: -> + @.isLoading = false + + _enableScroll : -> + @.scrollDisabled = false + + _disableScroll : -> + @.scrollDisabled = true + + _checkIfHasMorePages: (hasNext) -> + if hasNext + @._page += 1 + @._enableScroll() + else + @._disableScroll() + + _checkIfHasNoResults: -> + @.hasNoResults = @.items.size == 0 + + loadItems: -> + @._enableLoadingSpinner() + @._disableScroll() + + @._getItems(@.user.get("id"), @._page, @.type, @.q) + .then (response) => + @.items = @.items.concat(response.get("data")) + + @._checkIfHasMorePages(response.get("next")) + @._checkIfHasNoResults() + @._disableLoadingSpinner() + + return @.items + .catch => + @._disableLoadingSpinner() + + return @.items + + ################################################ + ## Filtre actions + ################################################ + filterByTextQuery: debounceLeading 500, -> + @._resetList() + @.loadItems() + + showAll: -> + if @.type isnt null + @.type = null + @._resetList() + @.loadItems() + + showProjectsOnly: -> + if @.type isnt "project" + @.type = "project" + @._resetList() + @.loadItems() + + showUserStoriesOnly: -> + if @.type isnt "userstory" + @.type = "userstory" + @._resetList() + @.loadItems() + + showTasksOnly: -> + if @.type isnt "task" + @.type = "task" + @._resetList() + @.loadItems() + + showIssuesOnly: -> + if @.type isnt "issue" + @.type = "issue" + @._resetList() + @.loadItems() + + +#################################################### +## Liked +#################################################### + +class ProfileLikedController extends FavsBaseController + @.$inject = [ + "tgUserService", + ] + + constructor: (@userService) -> + super() + @.enableFilterByAll = false + @.enableFilterByProjects = false + @.enableFilterByUserStories = false + @.enableFilterByTasks = false + @.enableFilterByIssues = false + @.enableFilterByTextQuery = true + @._getItems = @userService.getLiked + + +angular.module("taigaProfile") + .controller("ProfileLiked", ProfileLikedController) + +#################################################### +## Voted +#################################################### + +class ProfileVotedController extends FavsBaseController + @.$inject = [ + "tgUserService", + ] + + constructor: (@userService) -> + super() + @.enableFilterByAll = true + @.enableFilterByProjects = false + @.enableFilterByUserStories = true + @.enableFilterByTasks = true + @.enableFilterByIssues = true + @.enableFilterByTextQuery = true + @._getItems = @userService.getVoted + + +angular.module("taigaProfile") + .controller("ProfileVoted", ProfileVotedController) + + + +#################################################### +## Watched +#################################################### + +class ProfileWatchedController extends FavsBaseController + @.$inject = [ + "tgUserService", + ] + + constructor: (@userService) -> + super() + @._getItems = @userService.getWatched + + +angular.module("taigaProfile") + .controller("ProfileWatched", ProfileWatchedController) + diff --git a/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee b/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee new file mode 100644 index 00000000..70ac3409 --- /dev/null +++ b/app/modules/profile/profile-favs/profile-favs.controller.spec.coffee @@ -0,0 +1,698 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-favs.controller.spec.coffee +### + +describe "ProfileLiked", -> + $controller = null + provide = null + $rootScope = null + mocks = {} + + user = Immutable.fromJS({id: 2}) + + _mockUserService = () -> + mocks.userServices = { + getLiked: sinon.stub() + } + + provide.value "tgUserService", mocks.userServices + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockUserService() + + return null + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + beforeEach -> + module "taigaProfile" + _mocks() + _inject() + + it "load paginated items", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileLiked", $scope, {user: user}) + + items1 = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + items2 = Immutable.fromJS({ + data: [ + {id: 4}, + {id: 5}, + ], + next: false + }) + + mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, null).promise().resolve(items1) + mocks.userServices.getLiked.withArgs(user.get("id"), 2, null, null).promise().resolve(items2) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.loadItems().then () => + expectItems = items1.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.loadItems().then () => + expectItems = expectItems.concat(items2.get("data")) + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.true + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + done() + + it "filter items by text query", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileLiked", $scope, {user: user}) + + textQuery = "_test_" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, textQuery).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.q = textQuery + + ctrl.loadItems().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.equal(textQuery) + done() + + it "shou loading spinner during the call to the api", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileLiked", $scope, {user: user}) + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mockPromise = mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, null).promise() + + expect(ctrl.isLoading).to.be.undefined + + promise = ctrl.loadItems() + + expect(ctrl.isLoading).to.be.true + + mockPromise.resolve(items) + + promise.then () => + expect(ctrl.isLoading).to.be.false + done() + + it "shou no results placeholder", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileLiked", $scope, {user: user}) + + items = Immutable.fromJS({ + data: [], + next: false + }) + + mocks.userServices.getLiked.withArgs(user.get("id"), 1, null, null).promise().resolve(items) + + expect(ctrl.hasNoResults).to.be.undefined + + ctrl.loadItems().then () => + expect(ctrl.hasNoResults).to.be.true + done() + + +describe "ProfileVoted", -> + $controller = null + provide = null + $rootScope = null + mocks = {} + + user = Immutable.fromJS({id: 2}) + + _mockUserService = () -> + mocks.userServices = { + getVoted: sinon.stub() + } + + provide.value "tgUserService", mocks.userServices + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockUserService() + + return null + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + beforeEach -> + module "taigaProfile" + _mocks() + _inject() + + it "load paginated items", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + items1 = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + items2 = Immutable.fromJS({ + data: [ + {id: 4}, + {id: 5}, + ], + next: false + }) + + mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, null).promise().resolve(items1) + mocks.userServices.getVoted.withArgs(user.get("id"), 2, null, null).promise().resolve(items2) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.loadItems().then () => + expectItems = items1.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.loadItems().then () => + expectItems = expectItems.concat(items2.get("data")) + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.true + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + done() + + it "filter items by text query", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + textQuery = "_test_" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, textQuery).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.q = textQuery + + ctrl.loadItems().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.equal(textQuery) + done() + + it "show only items of user stories", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + type = "userstory" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getVoted.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showUserStoriesOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + + it "show only items of tasks", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + type = "task" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getVoted.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showTasksOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + + it "show only items of issues", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + type = "issue" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getVoted.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showIssuesOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + + it "shou loading spinner during the call to the api", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mockPromise = mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, null).promise() + + expect(ctrl.isLoading).to.be.undefined + + promise = ctrl.loadItems() + + expect(ctrl.isLoading).to.be.true + + mockPromise.resolve(items) + + promise.then () => + expect(ctrl.isLoading).to.be.false + done() + + it "shou no results placeholder", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileVoted", $scope, {user: user}) + + items = Immutable.fromJS({ + data: [], + next: false + }) + + mocks.userServices.getVoted.withArgs(user.get("id"), 1, null, null).promise().resolve(items) + + expect(ctrl.hasNoResults).to.be.undefined + + ctrl.loadItems().then () => + expect(ctrl.hasNoResults).to.be.true + done() + +describe "ProfileWatched", -> + $controller = null + provide = null + $rootScope = null + mocks = {} + + user = Immutable.fromJS({id: 2}) + + _mockUserService = () -> + mocks.userServices = { + getWatched: sinon.stub() + } + + provide.value "tgUserService", mocks.userServices + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockUserService() + + return null + + _inject = (callback) -> + inject (_$controller_, _$rootScope_) -> + $rootScope = _$rootScope_ + $controller = _$controller_ + + beforeEach -> + module "taigaProfile" + _mocks() + _inject() + + it "load paginated items", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + items1 = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + items2 = Immutable.fromJS({ + data: [ + {id: 4}, + {id: 5}, + ], + next: false + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, null).promise().resolve(items1) + mocks.userServices.getWatched.withArgs(user.get("id"), 2, null, null).promise().resolve(items2) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.loadItems().then () => + expectItems = items1.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.loadItems().then () => + expectItems = expectItems.concat(items2.get("data")) + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.true + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + done() + + it "filter items by text query", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + textQuery = "_test_" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, textQuery).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.q = textQuery + + ctrl.loadItems().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.equal(textQuery) + done() + + it "show only items of projects", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + type = "project" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showProjectsOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + + it "show only items of user stories", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + type = "userstory" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showUserStoriesOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + + it "show only items of tasks", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + type = "task" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showTasksOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + + it "show only items of issues", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + type = "issue" + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, type, null).promise().resolve(items) + + expect(ctrl.items.size).to.be.equal(0) + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.null + expect(ctrl.q).to.be.null + + ctrl.showIssuesOnly().then () => + expectItems = items.get("data") + + expect(ctrl.items.equals(expectItems)).to.be.true + expect(ctrl.scrollDisabled).to.be.false + expect(ctrl.type).to.be.type + expect(ctrl.q).to.be.null + done() + + it "shou loading spinner during the call to the api", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + items = Immutable.fromJS({ + data: [ + {id: 1}, + {id: 2}, + {id: 3} + ], + next: true + }) + + mockPromise = mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, null).promise() + + expect(ctrl.isLoading).to.be.undefined + + promise = ctrl.loadItems() + + expect(ctrl.isLoading).to.be.true + + mockPromise.resolve(items) + + promise.then () => + expect(ctrl.isLoading).to.be.false + done() + + it "shou no results placeholder", (done) -> + $scope = $rootScope.$new() + ctrl = $controller("ProfileWatched", $scope, {user: user}) + + items = Immutable.fromJS({ + data: [], + next: false + }) + + mocks.userServices.getWatched.withArgs(user.get("id"), 1, null, null).promise().resolve(items) + + expect(ctrl.hasNoResults).to.be.undefined + + ctrl.loadItems().then () => + expect(ctrl.hasNoResults).to.be.true + done() diff --git a/app/modules/profile/profile-favs/profile-favs.directive.coffee b/app/modules/profile/profile-favs/profile-favs.directive.coffee new file mode 100644 index 00000000..a7e07e45 --- /dev/null +++ b/app/modules/profile/profile-favs/profile-favs.directive.coffee @@ -0,0 +1,69 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-favs.directive.coffee +### + +base = { + scope: {}, + bindToController: { + user: "=" + type: "@" + q: "@" + scrollDisabled: "@" + isLoading: "@" + hasNoResults: "@" + } + controller: null, # Define in directives + controllerAs: "vm", + templateUrl: "profile/profile-favs/profile-favs.html", +} + + +#################################################### +## Liked +#################################################### + +ProfileLikedDirective = () -> + return _.extend({}, base, { + controller: "ProfileLiked" + }) + +angular.module("taigaProfile").directive("tgProfileLiked", ProfileLikedDirective) + + +#################################################### +## Voted +#################################################### + +ProfileVotedDirective = () -> + return _.extend({}, base, { + controller: "ProfileVoted" + }) + +angular.module("taigaProfile").directive("tgProfileVoted", ProfileVotedDirective) + + +#################################################### +## Watched +#################################################### + +ProfileWatchedDirective = () -> + return _.extend({}, base, { + controller: "ProfileWatched" + }) + +angular.module("taigaProfile").directive("tgProfileWatched", ProfileWatchedDirective) diff --git a/app/modules/profile/profile-favs/profile-favs.jade b/app/modules/profile/profile-favs/profile-favs.jade new file mode 100644 index 00000000..38d976e3 --- /dev/null +++ b/app/modules/profile/profile-favs/profile-favs.jade @@ -0,0 +1,80 @@ +section.profile-favs + div.profile-filter + div.searchbox(ng-if="::vm.enableFilterByTextQuery") + span.icon-search + input( + type="text" + ng-model="vm.q" + ng-change="vm.filterByTextQuery()" + placeholder="{{ 'USER.PROFILE_FAVS.FILTER_INPUT_PLACEHOLDER'|translate }}" + ) + + div.filters + a( + href="" + ng-if="::vm.enableFilterByAll" + ng-click="vm.showAll()" + ng-class="{active: vm.type === null}" + title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ALL_TITLE'|translate }}" + translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ALL'|translate }}" + ) + a( + href="" + ng-if="::vm.enableFilterByProjects" + ng-click="vm.showProjectsOnly()" + ng-class="{active: vm.type === 'project'}" + title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_PROJECTS_TITLE'|translate }}" + translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_PROJECTS'|translate }}" + ) + a( + href="" + ng-if="::vm.enableFilterByUserStories" + ng-click="vm.showUserStoriesOnly()" + ng-class="{active: vm.type === 'userstory'}", + title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_USER_STORIES_TITLE'|translate }}" + translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_USER_STORIES'|translate }}" + ) + a( + href="" + ng-if="::vm.enableFilterByTasks" + ng-click="vm.showTasksOnly()" + ng-class="{active: vm.type === 'task'}" + title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_TASKS_TITLE'|translate }}" + translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_TASKS'|translate }}" + ) + a( + href="" + ng-if="::vm.enableFilterByIssues" + ng-click="vm.showIssuesOnly()" + ng-class="{active: vm.type === 'issue'}" + title="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ISSUES_TITLE'|translate }}" + translate="{{ 'USER.PROFILE_FAVS.FILTER_TYPE_ISSUES'|translate }}" + ) + + div( + infinite-scroll="vm.loadItems()" + infinite-scroll-distance="2" + infinite-scroll-disabled="vm.scrollDisabled" + ) + div( + tg-repeat="item in vm.items track by $index" + ng-switch="item.get('type')" + ) + div(ng-switch-when="project", tg-fav-item="item", item-type="project") + div(ng-switch-when="userstory", tg-fav-item="item", item-type="userstory") + div(ng-switch-when="task", tg-fav-item="item", item-type="task") + div(ng-switch-when="issue", tg-fav-item="item", item-type="issue") + + div(ng-if="vm.isLoading") + div.spin + img( + src="/svg/spinner-circle.svg" + alt="{{ 'COMMON.LOADING'|translate }}" + ) + + .empty-search-results(ng-if="vm.hasNoResults && !vm.isLoading") + img( + src="../../images/search-empty.png" + alt="{{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }}" + ) + p.title {{ 'USER.PROFILE_FAVS.EMPTY_TITLE' | translate }} diff --git a/app/modules/profile/profile-favs/profile-favs.scss b/app/modules/profile/profile-favs/profile-favs.scss new file mode 100644 index 00000000..e37d4542 --- /dev/null +++ b/app/modules/profile/profile-favs/profile-favs.scss @@ -0,0 +1,43 @@ +.profile-favs { + border-top: 1px solid $whitish; +} + +.profile-filter { + align-items: center; + background: $whitish; + display: flex; + justify-content: space-between; + margin: 1rem 0; + padding: .5rem 1rem; + .searchbox { + align-items: center; + display: flex; + flex: 1; + .icon-search { + color: grayer; + margin-right: .5rem; + } + input { + border: 0; + border-bottom: 1px solid transparent; + flex: 1; + margin-right: 1rem; + &:focus { + border-bottom: 1px solid $gray-light; + outline: none; + transition: border-bottom .3s ease-in; + } + } + } + .filters { + a { + color: $gray-light; + display: inline-block; + padding: 0 .5rem; + &:hover, + &.active { + color: $blackish; + } + } + } +} diff --git a/app/modules/profile/profile-hints/profile-hints.controller.coffee b/app/modules/profile/profile-hints/profile-hints.controller.coffee index 8639a5df..95261959 100644 --- a/app/modules/profile/profile-hints/profile-hints.controller.coffee +++ b/app/modules/profile/profile-hints/profile-hints.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-hints.controller.coffee +### + class ProfileHints HINTS: [ { #hint1 diff --git a/app/modules/profile/profile-hints/profile-hints.controller.spec.coffee b/app/modules/profile/profile-hints/profile-hints.controller.spec.coffee index 15f3cc70..bc192e7d 100644 --- a/app/modules/profile/profile-hints/profile-hints.controller.spec.coffee +++ b/app/modules/profile/profile-hints/profile-hints.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-hints.controller.spec.coffee +### + describe "ProfileHints", -> $controller = null $provide = null diff --git a/app/modules/profile/profile-hints/profile-hints.directive.coffee b/app/modules/profile/profile-hints/profile-hints.directive.coffee index 63d61fcf..5b1f77ec 100644 --- a/app/modules/profile/profile-hints/profile-hints.directive.coffee +++ b/app/modules/profile/profile-hints/profile-hints.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-hints.directive.coffee +### + ProfileHints = ($translate) -> return { scope: {}, diff --git a/app/modules/profile/profile-projects/profile-projects.controller.coffee b/app/modules/profile/profile-projects/profile-projects.controller.coffee index cadcc2a1..26f2caa4 100644 --- a/app/modules/profile/profile-projects/profile-projects.controller.coffee +++ b/app/modules/profile/profile-projects/profile-projects.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-projects.controller.coffee +### + class ProfileProjectsController @.$inject = [ "tgProjectsService", diff --git a/app/modules/profile/profile-projects/profile-projects.controller.spec.coffee b/app/modules/profile/profile-projects/profile-projects.controller.spec.coffee index a090567f..9f8f0ee3 100644 --- a/app/modules/profile/profile-projects/profile-projects.controller.spec.coffee +++ b/app/modules/profile/profile-projects/profile-projects.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-projects.controller.spec.coffee +### + describe "ProfileProjects", -> $controller = null provide = null diff --git a/app/modules/profile/profile-projects/profile-projects.directive.coffee b/app/modules/profile/profile-projects/profile-projects.directive.coffee index 57e50153..848d655a 100644 --- a/app/modules/profile/profile-projects/profile-projects.directive.coffee +++ b/app/modules/profile/profile-projects/profile-projects.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-projects.directive.coffee +### + ProfileProjectsDirective = () -> link = (scope, elm, attr, ctrl) -> ctrl.loadProjects() diff --git a/app/modules/profile/profile-projects/profile-projects.jade b/app/modules/profile/profile-projects/profile-projects.jade index 4b5a8bd5..c9d10a0f 100644 --- a/app/modules/profile/profile-projects/profile-projects.jade +++ b/app/modules/profile/profile-projects/profile-projects.jade @@ -3,37 +3,56 @@ section.profile-projects div.spin img(src="/svg/spinner-circle.svg", alt="Loading...") - div.empty-tab(ng-if="vm.projects && !vm.projects.size") + .empty-tab(ng-if="vm.projects && !vm.projects.size") include ../../../svg/hide.svg - p(translate="USER.PROFILE.PROJECTS_EMPTY", translate-values="{username: vm.user.get('full_name_display')}") + p( + translate="USER.PROFILE.PROJECTS_EMPTY" + translate-values="{username: vm.user.get('full_name_display')}" + ) - div.project-list-single(tg-repeat="project in vm.projects") - div.project-list-single-left + .list-itemtype-project(tg-repeat="project in vm.projects") + .list-itemtype-project-left - div.project-list-single-title - h1 - a(href="#", tg-nav="project:project=project.get('slug')", title="{{ ::project.get('name') }}") {{::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 }} - div.project-list-single-tags.tags-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')") + .list-itemtype-project-tags.tags-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')}} - div.project-list-single-right + .list-itemtype-project-right - div.project-list-single-members - a(tg-repeat="contact in ::project.get('contacts')", tg-nav="user-profile:username=contact.get('username')", title="{{::contact.get('full_name')}}") + .list-itemtype-track + span.list-itemtype-track-likers( + ng-class="{'active': project.get('is_fan')}" + title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:project.get(\"total_fans\")||0}:'messageformat' }}" + ) + span.icon + include ../../../svg/like.svg + span {{ ::project.get('total_fans') }} + + span.list-itemtype-track-watchers( + ng-class="{'active': project.get('is_watcher')}" + title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:project.get(\"total_watchers\")||0}:'messageformat' }}" + ) + span.icon + include ../../../svg/watch.svg + span {{ ::project.get('total_watchers') }} + + .list-itemtype-project-members + a( + tg-repeat="contact in ::project.get('contacts')" + tg-nav="user-profile:username=contact.get('username')" + title="{{::contact.get('full_name')}}" + ) img(ng-src="{{::contact.get('photo')}}") - - // div.project-list-single-right - // div.project-list-single-stats - // div.stat-comments(title="2 comments") - // span.icon.icon-comment - // span.stat-num 2 - // div.stat-favorite.active(title="2 favorites") - // span.icon.icon-star-fill - // span.stat-num 4 - // div.stat-viewer(title="2 followers") - // span.icon.icon-open-eye - // span.stat-num 4 diff --git a/app/modules/profile/profile-tab/profile-tab.directive.coffee b/app/modules/profile/profile-tab/profile-tab.directive.coffee index 8312ddd4..070613d1 100644 --- a/app/modules/profile/profile-tab/profile-tab.directive.coffee +++ b/app/modules/profile/profile-tab/profile-tab.directive.coffee @@ -1,11 +1,32 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-tab.directive.coffee +### + ProfileTabDirective = () -> link = (scope, element, attrs, ctrl, transclude) -> scope.tab = {} + attrs.$observe "tgProfileTab", (name) -> + scope.tab.name = name + attrs.$observe "tabTitle", (title) -> scope.tab.title = title - scope.tab.name = attrs.tgProfileTab scope.tab.icon = attrs.tabIcon scope.tab.active = !!attrs.tabActive diff --git a/app/modules/profile/profile-tabs/profile-tabs.controller.coffee b/app/modules/profile/profile-tabs/profile-tabs.controller.coffee index 00ac82f5..1908f035 100644 --- a/app/modules/profile/profile-tabs/profile-tabs.controller.coffee +++ b/app/modules/profile/profile-tabs/profile-tabs.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-tabs.controller.coffee +### + class ProfileTabsController constructor: () -> @tabs = [] diff --git a/app/modules/profile/profile-tabs/profile-tabs.controller.spec.coffee b/app/modules/profile/profile-tabs/profile-tabs.controller.spec.coffee index 59312095..ff241a4a 100644 --- a/app/modules/profile/profile-tabs/profile-tabs.controller.spec.coffee +++ b/app/modules/profile/profile-tabs/profile-tabs.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-tabs.controller.spec.coffee +### + describe "ProfileTabsController", -> myCtrl = scope = null diff --git a/app/modules/profile/profile-tabs/profile-tabs.directive.coffee b/app/modules/profile/profile-tabs/profile-tabs.directive.coffee index 999e2900..6fe64f37 100644 --- a/app/modules/profile/profile-tabs/profile-tabs.directive.coffee +++ b/app/modules/profile/profile-tabs/profile-tabs.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile-tabs.directive.coffee +### + ProfileTabsDirective = () -> return { scope: {} diff --git a/app/modules/profile/profile-tabs/profile-tabs.jade b/app/modules/profile/profile-tabs/profile-tabs.jade index 00390cd8..ba10af3a 100644 --- a/app/modules/profile/profile-tabs/profile-tabs.jade +++ b/app/modules/profile/profile-tabs/profile-tabs.jade @@ -1,7 +1,13 @@ div nav.profile-content-tabs - a.tab(ng-repeat="tab in ::vm.tabs", href="", title="{{tab.title}}", ng-class="{active: tab.active}" ng-click="vm.toggleTab(tab)") + a.tab( + href="" + ng-repeat="tab in ::vm.tabs" + title="{{tab.title}}" + ng-click="vm.toggleTab(tab)" + ng-class="{active: tab.active}" + ) span.icon(ng-class="::tab.icon") span {{::tab.name}} - ng-transclude \ No newline at end of file + ng-transclude diff --git a/app/modules/profile/profile.controller.coffee b/app/modules/profile/profile.controller.coffee index 2c7350d6..b27049f6 100644 --- a/app/modules/profile/profile.controller.coffee +++ b/app/modules/profile/profile.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile.controller.coffee +### + class ProfileController @.$inject = [ "tgAppMetaService", diff --git a/app/modules/profile/profile.controller.spec.coffee b/app/modules/profile/profile.controller.spec.coffee index a8bb3a37..b75bac6a 100644 --- a/app/modules/profile/profile.controller.spec.coffee +++ b/app/modules/profile/profile.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile.controller.spec.coffee +### + describe "ProfileController", -> provide = null $controller = null diff --git a/app/modules/profile/profile.jade b/app/modules/profile/profile.jade index 5f0099d6..dc5ce3f7 100644 --- a/app/modules/profile/profile.jade +++ b/app/modules/profile/profile.jade @@ -1,18 +1,49 @@ -include ../../partials/includes/components/beta div.profile.centered(ng-if="vm.user") div(tg-profile-bar, user="vm.user", isCurrentUser="vm.isCurrentUser") div.main div.timeline-wrapper(tg-profile-tabs) - div(tg-profile-tab="activity", tab-title="{{'USER.PROFILE.ACTIVITY_TAB' | translate}}", tab-icon="icon-timeline", tab-active) - div(tg-user-timeline, user="vm.user", current-user="vm.isCurrentUser") + div( + tg-profile-tab="{{'USER.PROFILE.TABS.ACTIVITY_TAB' | translate}}" + tab-title="{{'USER.PROFILE.TABS.ACTIVITY_TAB_TITLE' | translate}}" + tab-icon="icon-timeline" + tab-active + ) + div(tg-user-timeline, user="vm.user", current-user="vm.isCurrentUser") - div(tab-disabled="{{vm.isCurrentUser}}", tg-profile-tab="projects", tab-title="{{'USER.PROFILE.PROJECTS_TAB' | translate}}", tab-icon="icon-project") - div(tg-profile-projects, user="vm.user") + div( + tg-profile-tab="{{'USER.PROFILE.TABS.PROJECTS_TAB' | translate}}" + tab-title="{{'USER.PROFILE.TABS.PROJECTS_TAB_TITLE' | translate}}" + tab-icon="icon-project" + tab-disabled="{{vm.isCurrentUser}}" + ) + div(tg-profile-projects, user="vm.user") - div(tg-profile-tab="contacts", tab-title="{{'USER.PROFILE.CONTACTS_TAB' | translate}}", tab-icon="icon-team") - div(tg-profile-contacts, user="vm.user") + div( + tg-profile-tab="{{'USER.PROFILE.TABS.LIKES_TAB' | translate}}" + tab-title="{{'USER.PROFILE.TABS.LIKES_TAB_TITLE' | translate}}" + tab-icon="icon-heart" + ) + div(tg-profile-liked, user="vm.user") - // div(tg-profile-tab="favorites", tab-title="{{'USER.PROFILE.FAVORITES_TAB' | translate}}", tab-icon="icon-star-fill") - // include includes/profile-favorites + div( + tg-profile-tab="{{'USER.PROFILE.TABS.VOTES_TAB' | translate}}" + tab-title="{{'USER.PROFILE.TABS.VOTES_TAB_TITLE' | translate}}" + tab-icon="icon-caret-up" + ) + div(tg-profile-voted, user="vm.user") + + div( + tg-profile-tab="{{'USER.PROFILE.TABS.WATCHED_TAB' | translate}}" + tab-title="{{'USER.PROFILE.TABS.WATCHED_TAB_TITLE' | translate}}" + tab-icon="icon-eye" + ) + div(tg-profile-watched, user="vm.user") + + div( + tg-profile-tab="{{'USER.PROFILE.TABS.CONTACTS_TAB' | translate}}" + tab-title="{{'USER.PROFILE.TABS.CONTACTS_TAB_TITLE' | translate}}" + tab-icon="icon-team" + ) + div(tg-profile-contacts, user="vm.user") include includes/profile-sidebar diff --git a/app/modules/profile/profile.module.coffee b/app/modules/profile/profile.module.coffee index 8c55b9f8..a37ea14d 100644 --- a/app/modules/profile/profile.module.coffee +++ b/app/modules/profile/profile.module.coffee @@ -1 +1,20 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: profile.module.coffee +### + module = angular.module("taigaProfile", []) diff --git a/app/modules/profile/styles/profile-bar.scss b/app/modules/profile/styles/profile-bar.scss index 2915ebcf..6cb27072 100644 --- a/app/modules/profile/styles/profile-bar.scss +++ b/app/modules/profile/styles/profile-bar.scss @@ -66,9 +66,10 @@ h1 { @extend %bold; @extend %xlarge; - line-height: 1; - margin-bottom: .5rem; + line-height: 1.2; + margin-bottom: .25rem; text-transform: none; + word-wrap: break-word; } .not-full-name { word-wrap: break-word; @@ -80,6 +81,12 @@ line-height: 1.2; margin-bottom: 1rem; } + .username { + @extend %light; + @extend %large; + color: $gray-light; + margin-bottom: 1rem; + } .location { color: $gray-light; margin-bottom: 1rem; diff --git a/app/modules/profile/styles/profile-contacts.scss b/app/modules/profile/styles/profile-contacts.scss index c9bce8e7..baa0dfe5 100644 --- a/app/modules/profile/styles/profile-contacts.scss +++ b/app/modules/profile/styles/profile-contacts.scss @@ -16,7 +16,7 @@ &:hover, &.active { border-bottom: 2px solid $gray-light; - color: $green-taiga; + color: $primary; } } } diff --git a/app/modules/profile/styles/profile-content-tabs.scss b/app/modules/profile/styles/profile-content-tabs.scss index 648d9d7f..492778cc 100644 --- a/app/modules/profile/styles/profile-content-tabs.scss +++ b/app/modules/profile/styles/profile-content-tabs.scss @@ -18,7 +18,7 @@ top: 1px; transition: color .2s linear; .icon { - color: $green-taiga; + color: $primary; transition: color .2s linear; } } diff --git a/app/modules/profile/styles/profile-favorites.scss b/app/modules/profile/styles/profile-favorites.scss deleted file mode 100644 index 23cb806b..00000000 --- a/app/modules/profile/styles/profile-favorites.scss +++ /dev/null @@ -1,21 +0,0 @@ -.profile-favorites { - border-top: 1px solid $whitish; - display: flex; - flex-direction: column; - .profile-favorites-filters { - align-self: flex-start; - display: flex; - a { - border-bottom: 2px solid $white; - color: $gray-light; - display: inline-block; - padding: 1rem 1.5rem; - transition: all .2s linear; - &:hover, - &.active { - border-bottom: 2px solid $gray-light; - color: $green-taiga; - } - } - } -} diff --git a/app/modules/profile/styles/profile-sidebar.scss b/app/modules/profile/styles/profile-sidebar.scss index 822fb599..944c813c 100644 --- a/app/modules/profile/styles/profile-sidebar.scss +++ b/app/modules/profile/styles/profile-sidebar.scss @@ -16,7 +16,7 @@ color: $gray-light; } a { - color: $green-taiga; + color: $primary; } .trans-button { margin-bottom: 1rem; diff --git a/app/modules/projects/components/like-project-button/like-project-button.controller.coffee b/app/modules/projects/components/like-project-button/like-project-button.controller.coffee new file mode 100644 index 00000000..e40bbd48 --- /dev/null +++ b/app/modules/projects/components/like-project-button/like-project-button.controller.coffee @@ -0,0 +1,59 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: like-project-button.controller.coffee +### + +class LikeProjectButtonController + @.$inject = [ + "$tgConfirm" + "tgLikeProjectButtonService" + ] + + constructor: (@confirm, @likeButtonService)-> + @.isMouseOver = false + @.loading = false + + showTextWhenMouseIsOver: -> + @.isMouseOver = true + + showTextWhenMouseIsLeave: -> + @.isMouseOver = false + + toggleLike: -> + @.loading = true + + if not @.project.get("is_fan") + promise = @._like() + else + promise = @._unlike() + + promise.finally () => @.loading = false + + return promise + + _like: -> + return @likeButtonService.like(@.project.get('id')) + .then => + @.showTextWhenMouseIsLeave() + .catch => + @confirm.notify("error") + + _unlike: -> + return @likeButtonService.unlike(@.project.get('id')).catch => + @confirm.notify("error") + +angular.module("taigaProjects").controller("LikeProjectButton", LikeProjectButtonController) diff --git a/app/modules/projects/components/like-project-button/like-project-button.controller.spec.coffee b/app/modules/projects/components/like-project-button/like-project-button.controller.spec.coffee new file mode 100644 index 00000000..b611a767 --- /dev/null +++ b/app/modules/projects/components/like-project-button/like-project-button.controller.spec.coffee @@ -0,0 +1,134 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: like-project-button.controller.spec.coffee +### + +describe "LikeProjectButton", -> + $provide = null + $controller = null + mocks = {} + + _mockTgConfirm = -> + mocks.tgConfirm = { + notify: sinon.stub() + } + + $provide.value("$tgConfirm", mocks.tgConfirm) + + _mockTgLikeProjectButton = -> + mocks.tgLikeProjectButton = { + like: sinon.stub(), + unlike: sinon.stub() + } + + $provide.value("tgLikeProjectButtonService", mocks.tgLikeProjectButton) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockTgConfirm() + _mockTgLikeProjectButton() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "toggleLike false -> true", (done) -> + project = Immutable.fromJS({ + id: 3, + is_fan: false + }) + + ctrl = $controller("LikeProjectButton") + ctrl.project = project + + mocks.tgLikeProjectButton.like = sinon.stub().promise() + + promise = ctrl.toggleLike() + + expect(ctrl.loading).to.be.true; + + mocks.tgLikeProjectButton.like.withArgs(project.get('id')).resolve() + + promise.finally () -> + expect(mocks.tgLikeProjectButton.like).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() + + it "toggleLike false -> true, notify error", (done) -> + project = Immutable.fromJS({ + id: 3, + is_fan: false + }) + + ctrl = $controller("LikeProjectButton") + ctrl.project = project + + mocks.tgLikeProjectButton.like.withArgs(project.get('id')).promise().reject() + + ctrl.toggleLike().finally () -> + expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce + done() + + it "toggleLike true -> false", (done) -> + project = Immutable.fromJS({ + is_fan: true + }) + + ctrl = $controller("LikeProjectButton") + ctrl.project = project + + mocks.tgLikeProjectButton.unlike = sinon.stub().promise() + + promise = ctrl.toggleLike() + + expect(ctrl.loading).to.be.true; + + mocks.tgLikeProjectButton.unlike.withArgs(project.get('id')).resolve() + + promise.finally () -> + expect(mocks.tgLikeProjectButton.unlike).to.be.calledOnce + expect(ctrl.loading).to.be.false; + + done() + + it "toggleLike true -> false, notify error", (done) -> + project = Immutable.fromJS({ + is_fan: true + }) + + ctrl = $controller("LikeProjectButton") + ctrl.project = project + + mocks.tgLikeProjectButton.unlike.withArgs(project.get('id')).promise().reject() + + ctrl.toggleLike().finally () -> + expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce + done() diff --git a/app/modules/projects/components/like-project-button/like-project-button.directive.coffee b/app/modules/projects/components/like-project-button/like-project-button.directive.coffee new file mode 100644 index 00000000..205d6f26 --- /dev/null +++ b/app/modules/projects/components/like-project-button/like-project-button.directive.coffee @@ -0,0 +1,31 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: like-project-button.directive.coffee +### + +LikeProjectButtonDirective = -> + return { + scope: {} + controller: "LikeProjectButton", + bindToController: { + project: '=' + } + controllerAs: "vm", + templateUrl: "projects/components/like-project-button/like-project-button.html", + } + +angular.module("taigaProjects").directive("tgLikeProjectButton", LikeProjectButtonDirective) diff --git a/app/modules/projects/components/like-project-button/like-project-button.jade b/app/modules/projects/components/like-project-button/like-project-button.jade new file mode 100644 index 00000000..7faad550 --- /dev/null +++ b/app/modules/projects/components/like-project-button/like-project-button.jade @@ -0,0 +1,29 @@ +a.track-button.like-button.like-container( + href="", + title="{{ 'PROJECT.LIKE_BUTTON.BUTTON_TITLE' | translate }}" + ng-click="vm.toggleLike()" + ng-class="{'active':vm.project.get('is_fan'), 'is-hover':vm.project.get('is_fan') && vm.isMouseOver}" + ng-mouseover="vm.showTextWhenMouseIsOver()" + ng-mouseleave="vm.showTextWhenMouseIsLeave()" +) + span.track-inner + span.track-icon + include ../../../../svg/like.svg + span( + ng-if="!vm.project.get('is_fan')" + translate="PROJECT.LIKE_BUTTON.LIKE" + ) + span( + ng-if="vm.project.get('is_fan') && !vm.isMouseOver" + translate="PROJECT.LIKE_BUTTON.LIKED" + ) + span( + ng-if="vm.project.get('is_fan') && vm.isMouseOver" + translate="PROJECT.LIKE_BUTTON.UNLIKE" + ) + + span.track-button-counter( + title="{{ 'PROJECT.LIKE_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_fans\")||0}:'messageformat' }}", + tg-loading="vm.loading" + ) + | {{ vm.project.get('total_fans') }} diff --git a/app/modules/projects/components/like-project-button/like-project-button.service.coffee b/app/modules/projects/components/like-project-button/like-project-button.service.coffee new file mode 100644 index 00000000..ec674e94 --- /dev/null +++ b/app/modules/projects/components/like-project-button/like-project-button.service.coffee @@ -0,0 +1,71 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: like-project-button.service.coffee +### + +taiga = @.taiga + +class LikeProjectButtonService extends taiga.Service + @.$inject = ["tgResources", "tgCurrentUserService", "tgProjectService"] + + constructor: (@rs, @currentUserService, @projectService) -> + + _getProjectIndex: (projectId) -> + return @currentUserService.projects + .get('all') + .findIndex (project) -> project.get('id') == projectId + + _updateProjects: (projectId, isFan) -> + projectIndex = @._getProjectIndex(projectId) + projects = @currentUserService.projects + .get('all') + .update projectIndex, (project) -> + + totalFans = project.get("total_fans") + + if isFan then totalFans++ else totalFans-- + + return project.merge({ + is_fan: isFan, + total_fans: totalFans + }) + + @currentUserService.setProjects(projects) + + _updateCurrentProject: (isFan) -> + totalFans = @projectService.project.get("total_fans") + + if isFan then totalFans++ else totalFans-- + + project = @projectService.project.merge({ + is_fan: isFan, + total_fans: totalFans + }) + + @projectService.setProject(project) + + like: (projectId) -> + return @rs.projects.likeProject(projectId).then => + @._updateProjects(projectId, true) + @._updateCurrentProject(true) + + unlike: (projectId) -> + return @rs.projects.unlikeProject(projectId).then => + @._updateProjects(projectId, false) + @._updateCurrentProject(false) + +angular.module("taigaProjects").service("tgLikeProjectButtonService", LikeProjectButtonService) diff --git a/app/modules/projects/components/like-project-button/like-project-button.service.spec.coffee b/app/modules/projects/components/like-project-button/like-project-button.service.spec.coffee new file mode 100644 index 00000000..c519a1f3 --- /dev/null +++ b/app/modules/projects/components/like-project-button/like-project-button.service.spec.coffee @@ -0,0 +1,151 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: like-project-button.service.spec.coffee +### + +describe "tgLikeProjectButtonService", -> + likeButtonService = null + provide = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + projects: { + likeProject: sinon.stub(), + unlikeProject: sinon.stub() + } + } + + provide.value "tgResources", mocks.tgResources + + _mockTgCurrentUserService = () -> + mocks.tgCurrentUserService = { + setProjects: sinon.stub(), + projects: Immutable.fromJS({ + all: [ + { + id: 4, + total_fans: 2, + is_fan: false + }, + { + id: 5, + total_fans: 7, + is_fan: true + }, + { + id: 6, + total_fans: 4, + is_fan: true + } + ] + }) + } + + provide.value "tgCurrentUserService", mocks.tgCurrentUserService + + _mockTgProjectService = () -> + mocks.tgProjectService = { + setProject: sinon.stub() + } + + provide.value "tgProjectService", mocks.tgProjectService + + _inject = (callback) -> + inject (_tgLikeProjectButtonService_) -> + likeButtonService = _tgLikeProjectButtonService_ + callback() if callback + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgResources() + _mockTgCurrentUserService() + _mockTgProjectService() + return null + + _setup = -> + _mocks() + + beforeEach -> + module "taigaProjects" + _setup() + _inject() + + it "like", (done) -> + projectId = 4 + + mocks.tgResources.projects.likeProject.withArgs(projectId).promise().resolve() + + newProject = { + id: 4, + total_fans: 3, + is_fan: true + } + + mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 0]) + + userServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable[0], newProject) + ), 'userServiceCheckImmutable' + + projectServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable, newProject) + ), 'projectServiceCheckImmutable' + + + likeButtonService.like(projectId).finally () -> + expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable) + expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable) + + done() + + it "unlike", (done) -> + projectId = 5 + + mocks.tgResources.projects.unlikeProject.withArgs(projectId).promise().resolve() + + newProject = { + id: 5, + total_fans: 6, + is_fan: false + } + + mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 1]) + + userServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable[1], newProject) + ), 'userServiceCheckImmutable' + + projectServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable, newProject) + ), 'projectServiceCheckImmutable' + + + likeButtonService.unlike(projectId).finally () -> + expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable) + expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable) + + done() diff --git a/app/modules/projects/components/sort-projects.directive.coffee b/app/modules/projects/components/sort-projects.directive.coffee index b25d4573..2c4cd753 100644 --- a/app/modules/projects/components/sort-projects.directive.coffee +++ b/app/modules/projects/components/sort-projects.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: sort-projects.directive.coffee +### + SortProjectsDirective = (currentUserService) -> link = (scope, el, attrs, ctrl) -> itemEl = null @@ -8,7 +27,7 @@ SortProjectsDirective = (currentUserService) -> axis: "y" opacity: .95 placeholder: 'placeholder' - cancel: '.project-name' + cancel: '.project-name' }) el.on "sortstop", (event, ui) -> diff --git a/app/modules/projects/components/watch-project-button/watch-project-button.controller.coffee b/app/modules/projects/components/watch-project-button/watch-project-button.controller.coffee new file mode 100644 index 00000000..d4635c63 --- /dev/null +++ b/app/modules/projects/components/watch-project-button/watch-project-button.controller.coffee @@ -0,0 +1,52 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-project-button.controller.coffee +### + +class WatchProjectButtonController + @.$inject = [ + "$tgConfirm" + "tgWatchProjectButtonService" + ] + + constructor: (@confirm, @watchButtonService)-> + @.showWatchOptions = false + @.loading = false + + toggleWatcherOptions: () -> + @.showWatchOptions = !@.showWatchOptions + + closeWatcherOptions: () -> + @.showWatchOptions = false + + watch: (notifyLevel) -> + @.loading = true + @.closeWatcherOptions() + + return @watchButtonService.watch(@.project.get('id'), notifyLevel) + .catch () => @confirm.notify("error") + .finally () => @.loading = false + + unwatch: -> + @.loading = true + @.closeWatcherOptions() + + return @watchButtonService.unwatch(@.project.get('id')) + .catch () => @confirm.notify("error") + .finally () => @.loading = false + +angular.module("taigaProjects").controller("WatchProjectButton", WatchProjectButtonController) diff --git a/app/modules/projects/components/watch-project-button/watch-project-button.controller.spec.coffee b/app/modules/projects/components/watch-project-button/watch-project-button.controller.spec.coffee new file mode 100644 index 00000000..0616178b --- /dev/null +++ b/app/modules/projects/components/watch-project-button/watch-project-button.controller.spec.coffee @@ -0,0 +1,155 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-project-button.controller.spec.coffee +### + +describe "WatchProjectButton", -> + $provide = null + $controller = null + mocks = {} + + _mockTgConfirm = -> + mocks.tgConfirm = { + notify: sinon.stub() + } + + $provide.value("$tgConfirm", mocks.tgConfirm) + + _mockTgWatchProjectButton = -> + mocks.tgWatchProjectButton = { + watch: sinon.stub(), + unwatch: sinon.stub() + } + + $provide.value("tgWatchProjectButtonService", mocks.tgWatchProjectButton) + + _mocks = -> + module (_$provide_) -> + $provide = _$provide_ + + _mockTgConfirm() + _mockTgWatchProjectButton() + + return null + + _inject = -> + inject (_$controller_) -> + $controller = _$controller_ + + _setup = -> + _mocks() + _inject() + + beforeEach -> + module "taigaProjects" + + _setup() + + it "toggleWatcherOption", () -> + ctrl = $controller("WatchProjectButton") + + ctrl.toggleWatcherOptions() + + expect(ctrl.showWatchOptions).to.be.true + + ctrl.toggleWatcherOptions() + + expect(ctrl.showWatchOptions).to.be.false + + it "watch", (done) -> + notifyLevel = 5 + project = Immutable.fromJS({ + id: 3 + }) + + ctrl = $controller("WatchProjectButton") + ctrl.project = project + ctrl.showWatchOptions = true + + mocks.tgWatchProjectButton.watch = sinon.stub().promise() + + promise = ctrl.watch(notifyLevel) + + expect(ctrl.loading).to.be.true + + mocks.tgWatchProjectButton.watch.withArgs(project.get('id'), notifyLevel).resolve() + + promise.finally () -> + expect(mocks.tgWatchProjectButton.watch).to.be.calledOnce + expect(ctrl.showWatchOptions).to.be.false + expect(ctrl.loading).to.be.false + + done() + + it "watch, notify error", (done) -> + notifyLevel = 5 + project = Immutable.fromJS({ + id: 3 + }) + + ctrl = $controller("WatchProjectButton") + ctrl.project = project + ctrl.showWatchOptions = true + + mocks.tgWatchProjectButton.watch.withArgs(project.get('id'), notifyLevel).promise().reject() + + ctrl.watch(notifyLevel).finally () -> + expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce + expect(ctrl.showWatchOptions).to.be.false + expect(ctrl.loading).to.be.false + + done() + + it "unwatch", (done) -> + project = Immutable.fromJS({ + id: 3 + }) + + ctrl = $controller("WatchProjectButton") + ctrl.project = project + ctrl.showWatchOptions = true + + mocks.tgWatchProjectButton.unwatch = sinon.stub().promise() + + promise = ctrl.unwatch() + + expect(ctrl.loading).to.be.true + + mocks.tgWatchProjectButton.unwatch.withArgs(project.get('id')).resolve() + + promise.finally () -> + expect(mocks.tgWatchProjectButton.unwatch).to.be.calledOnce + expect(ctrl.showWatchOptions).to.be.false + + done() + + it "unwatch, notify error", (done) -> + project = Immutable.fromJS({ + id: 3 + }) + + ctrl = $controller("WatchProjectButton") + ctrl.project = project + ctrl.showWatchOptions = true + + mocks.tgWatchProjectButton.unwatch.withArgs(project.get('id')).promise().reject() + + ctrl.unwatch().finally () -> + expect(mocks.tgConfirm.notify.withArgs("error")).to.be.calledOnce + expect(ctrl.showWatchOptions).to.be.false + + done() diff --git a/app/modules/projects/components/watch-project-button/watch-project-button.directive.coffee b/app/modules/projects/components/watch-project-button/watch-project-button.directive.coffee new file mode 100644 index 00000000..479e6bf4 --- /dev/null +++ b/app/modules/projects/components/watch-project-button/watch-project-button.directive.coffee @@ -0,0 +1,31 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-project-button.directive.coffee +### + +WatchProjectButtonDirective = -> + return { + scope: {} + controller: "WatchProjectButton", + bindToController: { + project: "=" + } + controllerAs: "vm", + templateUrl: "projects/components/watch-project-button/watch-project-button.html", + } + +angular.module("taigaProjects").directive("tgWatchProjectButton", WatchProjectButtonDirective) diff --git a/app/modules/projects/components/watch-project-button/watch-project-button.jade b/app/modules/projects/components/watch-project-button/watch-project-button.jade new file mode 100644 index 00000000..cd556a19 --- /dev/null +++ b/app/modules/projects/components/watch-project-button/watch-project-button.jade @@ -0,0 +1,56 @@ +a.track-button.watch-button.watch-container( + href="", + title="{{ 'PROJECT.WATCH_BUTTON.BUTTON_TITLE' | translate }}" + ng-click="vm.toggleWatcherOptions()" + ng-class="{'active': vm.project.get('is_watcher')}" +) + span.track-inner + span.track-icon + include ../../../../svg/watch.svg + span(ng-if="!vm.project.get('is_watcher')", translate="PROJECT.WATCH_BUTTON.WATCH") + span(ng-if="vm.project.get('is_watcher')", translate="PROJECT.WATCH_BUTTON.WATCHING") + span.icon.icon-arrow-up + + span.track-button-counter( + title="{{ 'PROJECT.WATCH_BUTTON.COUNTER_TITLE'|translate:{total:vm.project.get(\"total_watchers\")||0}:'messageformat' }}", + tg-loading="vm.loading" + ) + | {{ vm.project.get('total_watchers') }} + +ul.watch-options( + ng-class="{'hidden': !vm.showWatchOptions}" + ng-mouseleave="vm.closeWatcherOptions()" +) + //- NOTIFY LEVEL CHOICES: + //- 1 - Only involved + //- 2 - Receive all + //- 3 - No notifications + + li + a( + href="", + title="{{ 'PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_ALL_TITLE' | translate }}", + ng-click="vm.watch(2)", + ng-class="{'active': vm.project.get('is_watcher') && vm.project.get('notify_level') == 2}" + ) + span(translate="PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_ALL") + span.watch-check(ng-if="vm.project.get('is_watcher') && vm.project.get('notify_level') == 2") + include ../../../../svg/check.svg + li + a( + href="", + title="{{ 'PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_INVOLVED_TITLE' | translate }}", + ng-click="vm.watch(1)", + ng-class="{'active': vm.project.get('is_watcher') && vm.project.get('notify_level') == 1}" + ) + span(translate="PROJECT.WATCH_BUTTON.OPTIONS.NOTIFY_INVOLVED") + span.watch-check(ng-if="vm.project.get('is_watcher') && vm.project.get('notify_level') == 1") + include ../../../../svg/check.svg + + li(ng-if="vm.project.get('is_watcher')") + a( + href="", + title="{{ 'PROJECT.WATCH_BUTTON.OPTIONS.UNWATCH_TITLE' | translate }}", + ng-click="vm.unwatch()" + ) + span(translate="PROJECT.WATCH_BUTTON.OPTIONS.UNWATCH") diff --git a/app/modules/projects/components/watch-project-button/watch-project-button.service.coffee b/app/modules/projects/components/watch-project-button/watch-project-button.service.coffee new file mode 100644 index 00000000..fdb8d8ca --- /dev/null +++ b/app/modules/projects/components/watch-project-button/watch-project-button.service.coffee @@ -0,0 +1,78 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-project-button.service.coffee +### + +taiga = @.taiga + +class WatchProjectButtonService extends taiga.Service + @.$inject = [ + "tgResources", + "tgCurrentUserService", + "tgProjectService" + ] + + constructor: (@rs, @currentUserService, @projectService) -> + + _getProjectIndex: (projectId) -> + return @currentUserService.projects + .get('all') + .findIndex (project) -> project.get('id') == projectId + + + _updateProjects: (projectId, notifyLevel, isWatcher) -> + projectIndex = @._getProjectIndex(projectId) + + projects = @currentUserService.projects + .get('all') + .update projectIndex, (project) => + totalWatchers = project.get('total_watchers') + + if isWatcher then totalWatchers++ else totalWatchers-- + + return project.merge({ + is_watcher: isWatcher, + total_watchers: totalWatchers + notify_level: notifyLevel + }) + + @currentUserService.setProjects(projects) + + _updateCurrentProject: (notifyLevel, isWatcher) -> + totalWatchers = @projectService.project.get("total_watchers") + + if isWatcher then totalWatchers++ else totalWatchers-- + + project = @projectService.project.merge({ + is_watcher: isWatcher, + total_watchers: totalWatchers + notify_level: notifyLevel + }) + + @projectService.setProject(project) + + watch: (projectId, notifyLevel) -> + return @rs.projects.watchProject(projectId, notifyLevel).then => + @._updateProjects(projectId, notifyLevel, true) + @._updateCurrentProject(notifyLevel, true) + + unwatch: (projectId) -> + return @rs.projects.unwatchProject(projectId).then => + @._updateProjects(projectId, null, false) + @._updateCurrentProject(null, false) + +angular.module("taigaProjects").service("tgWatchProjectButtonService", WatchProjectButtonService) diff --git a/app/modules/projects/components/watch-project-button/watch-project-button.service.spec.coffee b/app/modules/projects/components/watch-project-button/watch-project-button.service.spec.coffee new file mode 100644 index 00000000..4b545b24 --- /dev/null +++ b/app/modules/projects/components/watch-project-button/watch-project-button.service.spec.coffee @@ -0,0 +1,161 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: watch-project-button.service.spec.coffee +### + +describe "tgWatchProjectButtonService", -> + watchButtonService = null + provide = null + mocks = {} + + _mockTgResources = () -> + mocks.tgResources = { + projects: { + watchProject: sinon.stub(), + unwatchProject: sinon.stub() + } + } + + provide.value "tgResources", mocks.tgResources + + _mockTgCurrentUserService = () -> + mocks.tgCurrentUserService = { + setProjects: sinon.stub(), + getUser: () -> + return Immutable.fromJS({ + id: 89 + }) + projects: Immutable.fromJS({ + all: [ + { + id: 4, + total_watchers: 0, + is_watcher: false, + notify_level: null + }, + { + id: 5, + total_watchers: 1, + is_watcher: true, + notify_level: 3 + }, + { + id: 6, + total_watchers: 0, + is_watcher: true, + notify_level: null + } + ] + }) + } + + provide.value "tgCurrentUserService", mocks.tgCurrentUserService + + _mockTgProjectService = () -> + mocks.tgProjectService = { + setProject: sinon.stub() + } + + provide.value "tgProjectService", mocks.tgProjectService + + _inject = (callback) -> + inject (_tgWatchProjectButtonService_) -> + watchButtonService = _tgWatchProjectButtonService_ + callback() if callback + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockTgResources() + _mockTgCurrentUserService() + _mockTgProjectService() + return null + + _setup = -> + _mocks() + + beforeEach -> + module "taigaProjects" + _setup() + _inject() + + it "watch", (done) -> + projectId = 4 + notifyLevel = 3 + + mocks.tgResources.projects.watchProject.withArgs(projectId, notifyLevel).promise().resolve() + + newProject = { + id: 4, + total_watchers: 1, + is_watcher: true, + notify_level: notifyLevel + } + + mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 0]) + + userServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable[0], newProject) + ), 'userServiceCheckImmutable' + + projectServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable, newProject) + ), 'projectServiceCheckImmutable' + + + watchButtonService.watch(projectId, notifyLevel).finally () -> + expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable) + expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable) + + done() + + it "unwatch", (done) -> + projectId = 5 + + mocks.tgResources.projects.unwatchProject.withArgs(projectId).promise().resolve() + + newProject = { + id: 5, + total_watchers: 0, + is_watcher: false, + notify_level: null + } + + mocks.tgProjectService.project = mocks.tgCurrentUserService.projects.getIn(['all', 1]) + + userServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable[1], newProject) + ), 'userServiceCheckImmutable' + + projectServiceCheckImmutable = sinon.match ((immutable) -> + immutable = immutable.toJS() + + return _.isEqual(immutable, newProject) + ), 'projectServiceCheckImmutable' + + + watchButtonService.unwatch(projectId).finally () -> + expect(mocks.tgCurrentUserService.setProjects).to.have.been.calledWith(userServiceCheckImmutable) + expect(mocks.tgProjectService.setProject).to.have.been.calledWith(projectServiceCheckImmutable) + + done() diff --git a/app/modules/projects/listing/projects-listing.controller.coffee b/app/modules/projects/listing/projects-listing.controller.coffee index d815fa5c..3f8fb291 100644 --- a/app/modules/projects/listing/projects-listing.controller.coffee +++ b/app/modules/projects/listing/projects-listing.controller.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: projects-listing.controller.coffee +### + class ProjectsListingController @.$inject = [ "tgCurrentUserService", diff --git a/app/modules/projects/listing/projects-listing.controller.spec.coffee b/app/modules/projects/listing/projects-listing.controller.spec.coffee index 58b61c79..49fbd9eb 100644 --- a/app/modules/projects/listing/projects-listing.controller.spec.coffee +++ b/app/modules/projects/listing/projects-listing.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: projects-listing.controller.spec.coffee +### + describe "ProjectsListingController", -> pageCtrl = null provide = null diff --git a/app/modules/projects/listing/projects-listing.jade b/app/modules/projects/listing/projects-listing.jade index f4760ff1..60dab648 100644 --- a/app/modules/projects/listing/projects-listing.jade +++ b/app/modules/projects/listing/projects-listing.jade @@ -11,17 +11,17 @@ div.project-list-wrapper.centered section.project-list-section div.project-list ul(tg-sort-projects="vm.projects") - li.project-list-single(tg-bind-scope, tg-repeat="project in vm.projects track by project.get('id')") - div.project-list-single-left - div.project-title - h1.project-name + 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.project-list-single-tags.tags-container(ng-if="::project.get('tags').size") + 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')") span.tag-name {{::tag.get('name')}} diff --git a/app/modules/projects/listing/projects-listing.scss b/app/modules/projects/listing/projects-listing.scss deleted file mode 100644 index 0b329d2d..00000000 --- a/app/modules/projects/listing/projects-listing.scss +++ /dev/null @@ -1,80 +0,0 @@ -.project-list-single { - border-bottom: 1px solid $whitish; - display: flex; - justify-content: space-between; - min-height: 9rem; - padding: 1rem; - position: relative; -} - -.project-list-single-left { - display: flex; - flex-direction: column; - padding-right: 1rem; - h1 { - @extend %text; - @extend %larger; - color: $gray; - display: inline-block; - margin-bottom: 0; - text-transform: none; - vertical-align: middle; - white-space: nowrap; - } - p { - @extend %text; - @extend %xsmall; - color: $gray; - margin-bottom: 0; - } - .project-list-single-tags { - align-self: flex-end; - display: flex; - flex: 3; - flex-wrap: wrap; - margin-top: .5rem; - } - .tag { - align-self: flex-end; - margin-right: .5rem; - padding: .5rem; - } -} - -.project-list-single-right { - flex-shrink: 0; - justify-content: space-between; - width: 200px; - .project-list-single-stats { - align-self: flex-end; - display: flex; - div { - color: $gray-light; - margin-right: .5rem; - .icon { - margin-right: .2rem; - vertical-align: center; - } - } - .active { - .icon { - color: $star-fill; - } - } - } - .project-list-single-members { - align-self: flex-end; - display: flex; - flex-direction: row-reverse; - flex-grow: 0; - flex-wrap: wrap-reverse; - margin-top: 1rem; - a { - display: block; } - img { - border-radius: .1rem; - margin-right: .3rem; - width: 34px; - } - } -} diff --git a/app/modules/projects/listing/styles/profile-projects.scss b/app/modules/projects/listing/styles/profile-projects.scss index 034c14c0..534e9738 100644 --- a/app/modules/projects/listing/styles/profile-projects.scss +++ b/app/modules/projects/listing/styles/profile-projects.scss @@ -1,10 +1,14 @@ .profile-projects { border-top: 1px solid $whitish; - .project-list-single { + .list-itemtype-project { display: flex; justify-content: space-between; min-height: 10rem; + .list-itemtype-project-right { + display: flex; + flex-direction: column; + flex-shrink: 0; + width: 200px; + } } } - - diff --git a/app/modules/projects/listing/styles/project-list.scss b/app/modules/projects/listing/styles/project-list.scss index 8ba28f14..daf97513 100644 --- a/app/modules/projects/listing/styles/project-list.scss +++ b/app/modules/projects/listing/styles/project-list.scss @@ -9,6 +9,7 @@ padding: .9rem 1rem; h1 { @extend %larger; + @extend %light; margin: 0; } } @@ -41,13 +42,12 @@ } .placeholder { background-color: lighten($whitish, 3%); - height: 7rem; - width: 100%; + height: 5rem; } - .project-list-single { - background: $white; + .list-itemtype-project { + background: rgba($white, .6); &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 63%); cursor: move; transition: background .3s; .drag { diff --git a/app/modules/projects/project/project.controller.coffee b/app/modules/projects/project/project.controller.coffee index 7d72f4cd..0582d5f3 100644 --- a/app/modules/projects/project/project.controller.coffee +++ b/app/modules/projects/project/project.controller.coffee @@ -1,31 +1,50 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: project.controller.coffee +### + class ProjectController @.$inject = [ - "tgProjectsService", "$routeParams", "tgAppMetaService", "$tgAuth", - "tgXhrErrorService", - "$translate" + "$translate", + "tgProjectService" ] - constructor: (@projectsService, @routeParams, @appMetaService, @auth, @xhrError, @translate) -> + constructor: (@routeParams, @appMetaService, @auth, @translate, @projectService) -> projectSlug = @routeParams.pslug @.user = @auth.userData - @projectsService - .getProjectBySlug(projectSlug) - .then (project) => - @.project = project - @._setMeta(@.project) + taiga.defineImmutableProperty @, "project", () => return @projectService.project + taiga.defineImmutableProperty @, "members", () => return @projectService.activeMembers - .catch (xhr) => - @xhrError.response(xhr) + @appMetaService.setfn @._setMeta.bind(this) _setMeta: (project)-> - ctx = {projectName: project.get("name")} + metas = {} - title = @translate.instant("PROJECT.PAGE_TITLE", ctx) - description = project.get("description") - @appMetaService.setAll(title, description) + return metas if !@.project + + ctx = {projectName: @.project.get("name")} + + metas.title = @translate.instant("PROJECT.PAGE_TITLE", ctx) + metas.description = @.project.get("description") + + return metas angular.module("taigaProjects").controller("Project", ProjectController) diff --git a/app/modules/projects/project/project.controller.spec.coffee b/app/modules/projects/project/project.controller.spec.coffee index 8bd7aa5f..c215916a 100644 --- a/app/modules/projects/project/project.controller.spec.coffee +++ b/app/modules/projects/project/project.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: project.controller.spec.coffee +### + describe "ProjectController", -> $controller = null $q = null @@ -5,16 +24,14 @@ describe "ProjectController", -> $rootScope = null mocks = {} - _mockProjectsService = () -> - mocks.projectService = { - getProjectBySlug: sinon.stub() - } + _mockProjectService = () -> + mocks.projectService = {} - provide.value "tgProjectsService", mocks.projectService + provide.value "tgProjectService", mocks.projectService _mockAppMetaService = () -> mocks.appMetaService = { - setAll: sinon.stub() + setfn: sinon.stub() } provide.value "tgAppMetaService", mocks.appMetaService @@ -31,13 +48,6 @@ describe "ProjectController", -> pslug: "project-slug" } - _mockXhrErrorService = () -> - mocks.xhrErrorService = { - response: sinon.spy() - } - - provide.value "tgXhrErrorService", mocks.xhrErrorService - _mockTranslate = () -> mocks.translate = {} mocks.translate.instant = sinon.stub() @@ -47,11 +57,10 @@ describe "ProjectController", -> _mocks = () -> module ($provide) -> provide = $provide - _mockProjectsService() + _mockProjectService() _mockRouteParams() _mockAppMetaService() _mockAuth() - _mockXhrErrorService() _mockTranslate() return null @@ -69,20 +78,20 @@ describe "ProjectController", -> it "set local user", () -> project = Immutable.fromJS({ name: "projectName" + members: [] }) - mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project) - ctrl = $controller "Project", $scope: {} expect(ctrl.user).to.be.equal(mocks.auth.userData) - it "set page title", (done) -> + it "set page title", () -> $scope = $rootScope.$new() project = Immutable.fromJS({ name: "projectName" - description: "projectDescription" + description: "projectDescription", + members: [] }) mocks.translate.instant @@ -91,37 +100,31 @@ describe "ProjectController", -> }) .returns('projectTitle') - mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project) + mocks.projectService.project = project ctrl = $controller("Project") - setTimeout ( -> - expect(mocks.appMetaService.setAll.calledWithExactly("projectTitle", "projectDescription")).to.be.true - done() - ) + metas = ctrl._setMeta(project) - it "set local project variable", (done) -> + expect(metas.title).to.be.equal('projectTitle') + expect(metas.description).to.be.equal('projectDescription') + expect(mocks.appMetaService.setfn).to.be.calledOnce + + it "set local project variable and members", () -> project = Immutable.fromJS({ name: "projectName" }) - mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project) + members = Immutable.fromJS([ + {is_active: true}, + {is_active: true}, + {is_active: true} + ]) + + mocks.projectService.project = project + mocks.projectService.activeMembers = members ctrl = $controller("Project") - setTimeout ( () -> - expect(ctrl.project).to.be.equal(project) - done() - ) - - it "handle project error", (done) -> - xhr = {code: 403} - - mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().reject(xhr) - - ctrl = $controller("Project") - - setTimeout (() -> - expect(mocks.xhrErrorService.response.withArgs(xhr)).to.be.calledOnce - done() - ) + expect(ctrl.project).to.be.equal(project) + expect(ctrl.members).to.be.equal(members) diff --git a/app/modules/projects/project/project.jade b/app/modules/projects/project/project.jade index e99fb1ec..9f48bf12 100644 --- a/app/modules/projects/project/project.jade +++ b/app/modules/projects/project/project.jade @@ -1,27 +1,65 @@ div.wrapper tg-project-menu - div.main.centered.single-project + div.centered.single-project section.single-project-intro - h1 - span.green(class="project-name") {{::vm.project.get("name")}} - span.private(ng-if="::vm.project.get('is_private')", title="{{'PROJECT.PRIVATE' | translate}}") - include ../../../svg/lock.svg + 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')") + 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 h2.title {{"PROJECT.SECTION.TEAM" | translate}} ul.involved-team - a(tg-nav="user-profile:username=member.get('username')", title="{{::member.get('full_name')}}", tg-repeat="member in ::vm.project.get('memberships')") - 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}}") + li(tg-repeat="member in vm.members") + a( + tg-nav="user-profile:username=member.get('username')", + title="{{::member.get('full_name')}}" + ) + img(ng-src="{{::member.get('photo')}}", alt="{{::member.get('full_name')}}") + //- + h2.title Organizations + div.involved-organization + a(href="", title="User Name") + img( + src="https://s3.amazonaws.com/uifaces/faces/twitter/dan_higham/48.jpg" + alt="{{member.full_name}}" + ) diff --git a/app/modules/projects/projects.module.coffee b/app/modules/projects/projects.module.coffee index 4597eb7d..1197dad8 100644 --- a/app/modules/projects/projects.module.coffee +++ b/app/modules/projects/projects.module.coffee @@ -1 +1,20 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: projects.module.coffee +### + angular.module("taigaProjects", []) diff --git a/app/modules/projects/projects.service.coffee b/app/modules/projects/projects.service.coffee index bbedc8c4..5ad7c827 100644 --- a/app/modules/projects/projects.service.coffee +++ b/app/modules/projects/projects.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: projects.service.coffee +### + taiga = @.taiga groupBy = @.taiga.groupBy diff --git a/app/modules/projects/projects.service.spec.coffee b/app/modules/projects/projects.service.spec.coffee index fbaee2fd..8e686d68 100644 --- a/app/modules/projects/projects.service.spec.coffee +++ b/app/modules/projects/projects.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: projects.service.spec.coffee +### + describe "tgProjectsService", -> projectsService = provide = $rootScope = null $q = null diff --git a/app/modules/resources/external-apps-resource.service.coffee b/app/modules/resources/external-apps-resource.service.coffee new file mode 100644 index 00000000..4c6f7430 --- /dev/null +++ b/app/modules/resources/external-apps-resource.service.coffee @@ -0,0 +1,46 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: external-apps-resource.service.coffee +### + +Resource = (urlsService, http) -> + service = {} + + service.getApplicationToken = (applicationId, state) -> + url = urlsService.resolve("applications") + url = "#{url}/#{applicationId}/token?state=#{state}" + return http.get(url).then (result) -> + Immutable.fromJS(result.data) + + service.authorizeApplicationToken = (applicationId, state) -> + url = urlsService.resolve("application-tokens") + url = "#{url}/authorize" + data = { + "state": state + "application": applicationId + } + + return http.post(url, data).then (result) -> + Immutable.fromJS(result.data) + + return () -> + return {"externalapps": service} + +Resource.$inject = ["$tgUrls", "$tgHttp"] + +module = angular.module("taigaResources2") +module.factory("tgExternalAppsResource", Resource) diff --git a/app/modules/resources/issues-resource.service.coffee b/app/modules/resources/issues-resource.service.coffee index 37213c60..7d4e6a26 100644 --- a/app/modules/resources/issues-resource.service.coffee +++ b/app/modules/resources/issues-resource.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: issues-resource.service.coffee +### + Resource = (urlsService, http) -> service = {} diff --git a/app/modules/resources/projects-resource.service.coffee b/app/modules/resources/projects-resource.service.coffee index 806bc550..4be78a45 100644 --- a/app/modules/resources/projects-resource.service.coffee +++ b/app/modules/resources/projects-resource.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: projects-resource.service.coffee +### + pagination = () -> Resource = (urlsService, http, paginateResponseService) -> @@ -51,6 +70,25 @@ Resource = (urlsService, http, paginateResponseService) -> result = Immutable.fromJS(result) return paginateResponseService(result) + service.likeProject = (projectId) -> + url = urlsService.resolve("project-like", projectId) + return http.post(url) + + service.unlikeProject = (projectId) -> + url = urlsService.resolve("project-unlike", projectId) + return http.post(url) + + service.watchProject = (projectId, notifyPolicy) -> + data = { + notify_policy: notifyPolicy + } + url = urlsService.resolve("project-watch", projectId) + return http.post(url, data) + + service.unwatchProject = (projectId) -> + url = urlsService.resolve("project-unwatch", projectId) + return http.post(url) + return () -> return {"projects": service} diff --git a/app/modules/resources/resources.coffee b/app/modules/resources/resources.coffee index b3fecee0..b2a0051b 100644 --- a/app/modules/resources/resources.coffee +++ b/app/modules/resources/resources.coffee @@ -1,9 +1,30 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: resources.coffee +### + services = [ "tgProjectsResources", + "tgUserResources", "tgUsersResources", "tgUserstoriesResource", "tgTasksResource", - "tgIssuesResource" + "tgIssuesResource", + "tgExternalAppsResource" ] Resources = ($injector) -> diff --git a/app/modules/resources/resources.module.coffee b/app/modules/resources/resources.module.coffee index b1fdef81..bafccdd5 100644 --- a/app/modules/resources/resources.module.coffee +++ b/app/modules/resources/resources.module.coffee @@ -1 +1,20 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: resources.module.coffee +### + angular.module("taigaResources2", []) diff --git a/app/modules/resources/tasks-resource.service.coffee b/app/modules/resources/tasks-resource.service.coffee index d122cebf..c1cb225d 100644 --- a/app/modules/resources/tasks-resource.service.coffee +++ b/app/modules/resources/tasks-resource.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: tasks-resource.service.coffee +### + Resource = (urlsService, http) -> service = {} diff --git a/app/modules/resources/user-resource.service.coffee b/app/modules/resources/user-resource.service.coffee new file mode 100644 index 00000000..063ba18d --- /dev/null +++ b/app/modules/resources/user-resource.service.coffee @@ -0,0 +1,60 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-resource.service.coffee +### + +Resource = (urlsService, http, paginateResponseService) -> + service = {} + + service.getUserStorage = (key) -> + url = urlsService.resolve("user-storage") + + if key + url += '/' + key + + httpOptions = {} + + return http.get(url, {}).then (response) -> + return response.data.value + + service.setUserStorage = (key, value) -> + url = urlsService.resolve("user-storage") + '/' + key + + params = { + key: key, + value: value + } + + return http.put(url, params) + + service.createUserStorage = (key, value) -> + url = urlsService.resolve("user-storage") + + params = { + key: key, + value: value + } + + return http.post(url, params) + + return () -> + return {"user": service} + +Resource.$inject = ["$tgUrls", "$tgHttp"] + +module = angular.module("taigaResources2") +module.factory("tgUserResources", Resource) diff --git a/app/modules/resources/users-resource.service.coffee b/app/modules/resources/users-resource.service.coffee index 57e0a8eb..6d04bfa1 100644 --- a/app/modules/resources/users-resource.service.coffee +++ b/app/modules/resources/users-resource.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: users-resource.service.coffee +### + Resource = (urlsService, http, paginateResponseService) -> service = {} @@ -19,7 +38,7 @@ Resource = (urlsService, http, paginateResponseService) -> return Immutable.fromJS(result.data) service.getStats = (userId) -> - url = urlsService.resolve("stats", userId) + url = urlsService.resolve("user-stats", userId) httpOptions = { headers: { @@ -32,7 +51,7 @@ Resource = (urlsService, http, paginateResponseService) -> return Immutable.fromJS(result.data) service.getContacts = (userId) -> - url = urlsService.resolve("contacts", userId) + url = urlsService.resolve("user-contacts", userId) httpOptions = { headers: { @@ -44,6 +63,45 @@ Resource = (urlsService, http, paginateResponseService) -> .then (result) -> return Immutable.fromJS(result.data) + service.getLiked = (userId, page, type, q) -> + url = urlsService.resolve("user-liked", userId) + + params = {} + params.page = page if page? + params.type = type if type? + params.q = q if q? + + return http.get(url, params) + .then (result) -> + result = Immutable.fromJS(result) + return paginateResponseService(result) + + service.getVoted = (userId, page, type, q) -> + url = urlsService.resolve("user-voted", userId) + + params = {} + params.page = page if page? + params.type = type if type? + params.q = q if q? + + return http.get(url, params) + .then (result) -> + result = Immutable.fromJS(result) + return paginateResponseService(result) + + service.getWatched = (userId, page, type, q) -> + url = urlsService.resolve("user-watched", userId) + + params = {} + params.page = page if page? + params.type = type if type? + params.q = q if q? + + return http.get(url, params) + .then (result) -> + result = Immutable.fromJS(result) + return paginateResponseService(result) + service.getProfileTimeline = (userId, page) -> params = { page: page diff --git a/app/modules/resources/userstories-resource.service.coffee b/app/modules/resources/userstories-resource.service.coffee index dccd35fd..22f68cae 100644 --- a/app/modules/resources/userstories-resource.service.coffee +++ b/app/modules/resources/userstories-resource.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: userstories-resource.service.coffee +### + Resource = (urlsService, http) -> service = {} diff --git a/app/modules/services/app-meta.service.coffee b/app/modules/services/app-meta.service.coffee index ca31dcb5..7c5ae20e 100644 --- a/app/modules/services/app-meta.service.coffee +++ b/app/modules/services/app-meta.service.coffee @@ -1,9 +1,34 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: app-meta.service.coffee +### + taiga = @.taiga truncate = taiga.truncate -class AppMetaService extends taiga.Service = -> +class AppMetaService + @.$inject = [ + "$rootScope" + ] + + constructor: (@rootScope) -> + _set: (key, value) -> return if not key @@ -59,5 +84,20 @@ class AppMetaService extends taiga.Service = -> @.setTwitterMetas(title, description) @.setOpenGraphMetas(title, description) + addMobileViewport: () -> + $("head").append( + "" + ) + + removeMobileViewport: () -> + $("meta[name=\"viewport\"]").remove() + + setfn: (fn) -> + @._listener() if @.listener + + @._listener = @rootScope.$watchCollection fn, (metas) => + @.setAll(metas.title, metas.description) + angular.module("taigaCommon").service("tgAppMetaService", AppMetaService) diff --git a/app/modules/services/app-meta.service.spec.coffee b/app/modules/services/app-meta.service.spec.coffee index ede2294d..a8ae97ea 100644 --- a/app/modules/services/app-meta.service.spec.coffee +++ b/app/modules/services/app-meta.service.spec.coffee @@ -1,13 +1,34 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: app-meta.service.spec.coffee +### + describe "AppMetaService", -> appMetaService = null + $rootScope = null data = { title: "--title--", description: "--description--" } _inject = () -> - inject (_tgAppMetaService_) -> + inject (_tgAppMetaService_, _$rootScope_) -> appMetaService = _tgAppMetaService_ + $rootScope = _$rootScope_ beforeEach -> module "taigaCommon" @@ -53,3 +74,18 @@ describe "AppMetaService", -> expect($("meta[property='og:description']")).to.have.attr("content", data.description) expect($("meta[property='og:image']")).to.have.attr("content", "#{window.location.origin}/images/logo-color.png") expect($("meta[property='og:url']")).to.have.attr("content", window.location.href) + + it "set function to set the metas", () -> + fn = () -> + return { + title: 'test', + description: 'test2' + } + + + appMetaService.setAll = sinon.stub() + appMetaService.setfn(fn) + + $rootScope.$digest() + + expect(appMetaService.setAll).to.have.been.calledWith('test', 'test2') diff --git a/app/plugins/humanshtml/humanshtml.coffee b/app/modules/services/check-permissions.service.coffee similarity index 56% rename from app/plugins/humanshtml/humanshtml.coffee rename to app/modules/services/check-permissions.service.coffee index 59130f8a..c96fd0cc 100644 --- a/app/plugins/humanshtml/humanshtml.coffee +++ b/app/modules/services/check-permissions.service.coffee @@ -1,7 +1,5 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Taiga Agile LLC # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -16,16 +14,19 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -# File: plugins/humanshtml/humanshtml.coffee +# File: check-permissions.service.coffee ### -# Simple plugin example that extends the urlmappings -# and add additional template. - taiga = @.taiga -module = angular.module("taigaPlugins") -configure = ($routeProvider) -> - $routeProvider.when("/humans.html", {"templateUrl": "/plugins/humanshtml/templates/humans.html"}) +class ChekcPermissionsService + @.$inject = [ + "tgProjectService" + ] -module.config(["$routeProvider", configure]) + constructor: (@projectService) -> + + check: (permission) -> + return @projectService.project.get('my_permissions').indexOf(permission) != -1 + +angular.module("taigaCommon").service("tgCheckPermissionsService", ChekcPermissionsService) diff --git a/app/modules/services/check-permissions.service.spec.coffee b/app/modules/services/check-permissions.service.spec.coffee new file mode 100644 index 00000000..160e8e8c --- /dev/null +++ b/app/modules/services/check-permissions.service.spec.coffee @@ -0,0 +1,63 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: check-permissions.service.spec.coffee +### + +describe "tgCheckPermissionsService", -> + checkPermissionsService = provide = null + mocks = {} + + _mockProjectService = () -> + mocks.projectService = { + project: sinon.stub() + } + + provide.value "tgProjectService", mocks.projectService + + _inject = () -> + inject (_tgCheckPermissionsService_) -> + checkPermissionsService = _tgCheckPermissionsService_ + + _mocks = () -> + module ($provide) -> + provide = $provide + _mockProjectService() + + return null + + beforeEach -> + module "taigaCommon" + _mocks() + _inject() + + it "the user has perms", () -> + mocks.projectService.project = Immutable.fromJS({ + my_permissions: ['add_us'] + }) + + perm = checkPermissionsService.check('add_us') + + expect(perm).to.be.true + + it "the user hasn't perms", () -> + mocks.projectService.project = Immutable.fromJS({ + my_permissions: [] + }) + + perm = checkPermissionsService.check('add_us') + + expect(perm).to.be.false diff --git a/app/modules/services/current-user.service.coffee b/app/modules/services/current-user.service.coffee index 924ca587..dbd2ee90 100644 --- a/app/modules/services/current-user.service.coffee +++ b/app/modules/services/current-user.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: current-user.service.coffee +### + taiga = @.taiga groupBy = @.taiga.groupBy @@ -5,13 +24,15 @@ groupBy = @.taiga.groupBy class CurrentUserService @.$inject = [ "tgProjectsService", - "$tgStorage" + "$tgStorage", + "tgResources" ] - constructor: (@projectsService, @storageService) -> + constructor: (@projectsService, @storageService, @rs) -> @._user = null @._projects = Immutable.Map() @._projectsById = Immutable.Map() + @._joyride = null taiga.defineImmutableProperty @, "projects", () => return @._projects taiga.defineImmutableProperty @, "projectsById", () => return @._projectsById @@ -35,6 +56,7 @@ class CurrentUserService @._user = null @._projects = Immutable.Map() @._projectsById = Immutable.Map() + @._joyride = null setUser: (user) -> @._user = user @@ -43,19 +65,57 @@ class CurrentUserService bulkUpdateProjectsOrder: (sortData) -> @projectsService.bulkUpdateProjectsOrder(sortData).then () => - @._loadProjects() + @.loadProjects() - _loadProjects: () -> + loadProjects: () -> return @projectsService.getProjectsByUserId(@._user.get("id")) - .then (projects) => - @._projects = @._projects.set("all", projects) - @._projects = @._projects.set("recents", projects.slice(0, 10)) + .then (projects) => @.setProjects(projects) - @._projectsById = Immutable.fromJS(groupBy(projects.toJS(), (p) -> p.id)) + disableJoyRide: (section) -> + if section + @._joyride[section] = false + else + @._joyride = { + backlog: false, + kanban: false, + dashboard: false + } - return @.projects + @rs.user.setUserStorage('joyride', @._joyride) + + loadJoyRideConfig: () -> + return new Promise (resolve) => + if @._joyride != null + resolve(@._joyride) + return + + @rs.user.getUserStorage('joyride') + .then (config) => + @._joyride = config + resolve(@._joyride) + .catch () => + #joyride not defined + @._joyride = { + backlog: true, + kanban: true, + dashboard: true + } + + @rs.user.createUserStorage('joyride', @._joyride) + + resolve(@._joyride) _loadUserInfo: () -> - return @._loadProjects() + return Promise.all([ + @.loadProjects() + ]) + + setProjects: (projects) -> + @._projects = @._projects.set("all", projects) + @._projects = @._projects.set("recents", projects.slice(0, 10)) + + @._projectsById = Immutable.fromJS(groupBy(projects.toJS(), (p) -> p.id)) + + return @.projects angular.module("taigaCommon").service("tgCurrentUserService", CurrentUserService) diff --git a/app/modules/services/current-user.service.spec.coffee b/app/modules/services/current-user.service.spec.coffee index ab8ea8f5..8ebf1b9b 100644 --- a/app/modules/services/current-user.service.spec.coffee +++ b/app/modules/services/current-user.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: current-user.service.spec.coffee +### + describe "tgCurrentUserService", -> currentUserService = provide = null mocks = {} @@ -17,6 +36,17 @@ describe "tgCurrentUserService", -> provide.value "tgProjectsService", mocks.projectsService + _mockResources = () -> + mocks.resources = { + user: { + setUserStorage: sinon.stub(), + getUserStorage: sinon.stub(), + createUserStorage: sinon.stub() + } + } + + provide.value "tgResources", mocks.resources + _inject = (callback) -> inject (_tgCurrentUserService_) -> currentUserService = _tgCurrentUserService_ @@ -27,6 +57,7 @@ describe "tgCurrentUserService", -> provide = $provide _mockTgStorage() _mockProjectsService() + _mockResources() return null @@ -80,15 +111,50 @@ describe "tgCurrentUserService", -> it "bulkUpdateProjectsOrder and reload projects", (done) -> fakeData = [{id: 1, id: 2}] - currentUserService._loadProjects = sinon.spy() + currentUserService.loadProjects = sinon.stub() mocks.projectsService.bulkUpdateProjectsOrder.withArgs(fakeData).promise().resolve() currentUserService.bulkUpdateProjectsOrder(fakeData).then () -> - expect(currentUserService._loadProjects).to.be.callOnce + expect(currentUserService.loadProjects).to.be.callOnce done() + it "loadProject and set it", (done) -> + user = Immutable.fromJS({id: 1, name: "fake1"}) + project = Immutable.fromJS({id: 2, name: "fake2"}) + + currentUserService._user = user + currentUserService.setProjects = sinon.stub() + + mocks.projectsService.getProjectsByUserId.withArgs(1).promise().resolve(project) + + currentUserService.loadProjects().then () -> + expect(currentUserService.setProjects).to.have.been.calledWith(project) + + done() + + it "setProject", () -> + projectsRaw = [ + {id: 1, name: "fake1"}, + {id: 2, name: "fake2"}, + {id: 3, name: "fake3"}, + {id: 4, name: "fake4"} + ] + projectsRawById = { + 1: {id: 1, name: "fake1"}, + 2: {id: 2, name: "fake2"}, + 3: {id: 3, name: "fake3"}, + 4: {id: 4, name: "fake4"} + } + projects = Immutable.fromJS(projectsRaw) + + currentUserService.setProjects(projects) + + expect(currentUserService.projects.get('all').toJS()).to.be.eql(projectsRaw) + expect(currentUserService.projects.get('recents').toJS()).to.be.eql(projectsRaw) + expect(currentUserService.projectsById.toJS()).to.be.eql(projectsRawById) + it "is authenticated", () -> currentUserService.getUser = sinon.stub() currentUserService.getUser.returns({}) @@ -105,3 +171,35 @@ describe "tgCurrentUserService", -> currentUserService.removeUser() expect(currentUserService._user).to.be.null + + it "disable joyride", () -> + currentUserService.disableJoyRide() + + expect(mocks.resources.user.setUserStorage).to.have.been.calledWith('joyride', { + backlog: false, + kanban: false, + dashboard: false + }); + + it "load joyride config", (done) -> + mocks.resources.user.getUserStorage.withArgs('joyride').promise().resolve(true) + + currentUserService.loadJoyRideConfig().then (config) -> + expect(config).to.be.true + + done() + + it "create default joyride config", (done) -> + mocks.resources.user.getUserStorage.withArgs('joyride').promise().reject() + + currentUserService.loadJoyRideConfig().then (config) -> + joyride = { + backlog: true, + kanban: true, + dashboard: true + } + + expect(mocks.resources.user.createUserStorage).to.have.been.calledWith('joyride', joyride) + expect(config).to.be.eql(joyride) + + done() diff --git a/app/modules/services/lightbox-factory.service.coffee b/app/modules/services/lightbox-factory.service.coffee index 9698fe27..f71b4ff3 100644 --- a/app/modules/services/lightbox-factory.service.coffee +++ b/app/modules/services/lightbox-factory.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: lightbox-factory.service.coffee +### + class LightboxFactory @.$inject = ["$rootScope", "$compile"] constructor: (@rootScope, @compile) -> diff --git a/app/modules/services/lightbox-factory.service.spec.coffee b/app/modules/services/lightbox-factory.service.spec.coffee index 40d2ccd7..8762f390 100644 --- a/app/modules/services/lightbox-factory.service.spec.coffee +++ b/app/modules/services/lightbox-factory.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: lightbox-factory.service.spec.coffee +### + describe "tgLightboxFactory", -> lightboxFactoryService = provide = null mocks = {} diff --git a/app/modules/services/paginate-response.service.coffee b/app/modules/services/paginate-response.service.coffee index d2e70d77..c27fc738 100644 --- a/app/modules/services/paginate-response.service.coffee +++ b/app/modules/services/paginate-response.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: paginate-response.service.coffee +### + PaginateResponse = () -> return (result) -> paginateResponse = Immutable.Map({ diff --git a/app/modules/services/paginate-response.service.spec.coffee b/app/modules/services/paginate-response.service.spec.coffee index d63dd962..a4d89fcd 100644 --- a/app/modules/services/paginate-response.service.spec.coffee +++ b/app/modules/services/paginate-response.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: paginate-response.service.spec.coffee +### + describe "PaginateResponseService", -> paginateResponseService = null diff --git a/app/modules/services/project.service.coffee b/app/modules/services/project.service.coffee index 9fe2902c..3294db67 100644 --- a/app/modules/services/project.service.coffee +++ b/app/modules/services/project.service.coffee @@ -1,18 +1,40 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: project.service.coffee +### + taiga = @.taiga class ProjectService @.$inject = [ - "tgProjectsService" + "tgProjectsService", + "tgXhrErrorService" ] - constructor: (@projectsService) -> + constructor: (@projectsService, @xhrError) -> @._project = null @._section = null @._sectionsBreadcrumb = Immutable.List() + @._activeMembers = Immutable.List() taiga.defineImmutableProperty @, "project", () => return @._project taiga.defineImmutableProperty @, "section", () => return @._section taiga.defineImmutableProperty @, "sectionsBreadcrumb", () => return @._sectionsBreadcrumb + taiga.defineImmutableProperty @, "activeMembers", () => return @._activeMembers setSection: (section) -> @._section = section @@ -22,20 +44,32 @@ class ProjectService else @._sectionsBreadcrumb = Immutable.List() - setProject: (pslug) -> - if @._pslug != pslug - @._pslug = pslug + setProjectBySlug: (pslug) -> + return new Promise (resolve, reject) => + if !@.project || @.project.get('slug') != pslug + @projectsService + .getProjectBySlug(pslug) + .then (project) => + @.setProject(project) + resolve() + .catch (xhr) => + @xhrError.response(xhr) - @.fetchProject() + else resolve() + + setProject: (project) -> + @._project = project + @._activeMembers = @._project.get('members').filter (member) -> member.get('is_active') cleanProject: () -> - @._pslug = null @._project = null + @._activeMembers = Immutable.List() @._section = null @._sectionsBreadcrumb = Immutable.List() fetchProject: () -> - return @projectsService.getProjectBySlug(@._pslug).then (project) => - @._project = project + pslug = @.project.get('slug') + + return @projectsService.getProjectBySlug(pslug).then (project) => @.setProject(project) angular.module("taigaCommon").service("tgProjectService", ProjectService) diff --git a/app/modules/services/project.service.spec.coffee b/app/modules/services/project.service.spec.coffee index 25016848..715f8032 100644 --- a/app/modules/services/project.service.spec.coffee +++ b/app/modules/services/project.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: project.service.spec.coffee +### + describe "tgProjectService", -> $provide = null mocks = {} @@ -10,11 +29,19 @@ describe "tgProjectService", -> $provide.value "tgProjectsService", mocks.projectsService + _mockXhrErrorService = () -> + mocks.xhrErrorService = { + response: sinon.stub() + } + + $provide.value "tgXhrErrorService", mocks.xhrErrorService + _mocks = () -> module (_$provide_) -> $provide = _$provide_ _mockProjectsService() + _mockXhrErrorService() return null @@ -46,31 +73,70 @@ describe "tgProjectService", -> expect(projectService.sectionsBreadcrumb.toJS()).to.be.eql(breadcrumb) - it "set project if the project slug has changed", () -> - projectService.fetchProject = sinon.spy() + it "set project if the project slug has changed", (done) -> + projectService.setProject = sinon.spy() - pslug = "slug-1" + project = Immutable.Map({ + id: 1, + slug: 'slug-1', + members: [] + }) - projectService.setProject(pslug) + mocks.projectsService.getProjectBySlug.withArgs('slug-1').promise().resolve(project) + mocks.projectsService.getProjectBySlug.withArgs('slug-2').promise().resolve(project) - expect(projectService.fetchProject).to.be.calledOnce + projectService.setProjectBySlug('slug-1') + .then () -> projectService.setProjectBySlug('slug-1') + .then () -> projectService.setProjectBySlug('slug-2') + .finally () -> + expect(projectService.setProject).to.be.called.twice; + done() - projectService.setProject(pslug) + it "set project and set active members", () -> + project = Immutable.fromJS({ + name: 'test project', + members: [ + {is_active: true}, + {is_active: false}, + {is_active: true}, + {is_active: false}, + {is_active: false} + ] + }) - expect(projectService.fetchProject).to.be.calledOnce + projectService.setProject(project) - projectService.setProject("slug-2") - - expect(projectService.fetchProject).to.be.calledTwice + expect(projectService.project).to.be.equal(project) + expect(projectService.activeMembers.size).to.be.equal(2) it "fetch project", (done) -> - project = Immutable.Map({id: 1}) - pslug = "slug-1" + project = Immutable.Map({ + id: 1, + slug: 'slug', + members: [] + }) - projectService._pslug = pslug + projectService._project = project - mocks.projectsService.getProjectBySlug.withArgs(pslug).promise().resolve(project) + mocks.projectsService.getProjectBySlug.withArgs(project.get('slug')).promise().resolve(project) projectService.fetchProject().then () -> expect(projectService.project).to.be.equal(project) done() + + it "clean project", () -> + projectService._section = "fakeSection" + projectService._sectionsBreadcrumb = ["fakeSection"] + projectService._activeMembers = ["fakeMember"] + projectService._project = Immutable.Map({ + id: 1, + slug: 'slug', + members: [] + }) + + projectService.cleanProject() + + expect(projectService.project).to.be.null; + expect(projectService.activeMembers.size).to.be.equal(0); + expect(projectService.section).to.be.null; + expect(projectService.sectionsBreadcrumb.size).to.be.equal(0); diff --git a/app/modules/services/scope-event.service.coffee b/app/modules/services/scope-event.service.coffee index 922ca6f3..f49e9f5d 100644 --- a/app/modules/services/scope-event.service.coffee +++ b/app/modules/services/scope-event.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: scope-event.service.coffee +### + class ScopeEvent scopes: {}, _searchDuplicatedScopes: (id) -> diff --git a/app/modules/services/scope-event.service.spec.coffee b/app/modules/services/scope-event.service.spec.coffee index 0f898b4c..338fdd27 100644 --- a/app/modules/services/scope-event.service.spec.coffee +++ b/app/modules/services/scope-event.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: scope-event.service.spec.coffee +### + angular.module("taigaCommon").provider("$exceptionHandler", angular.mock.$ExceptionHandlerProvider) describe "tgScopeEvent", -> diff --git a/app/modules/services/theme.service.coffee b/app/modules/services/theme.service.coffee new file mode 100644 index 00000000..205a396d --- /dev/null +++ b/app/modules/services/theme.service.coffee @@ -0,0 +1,34 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: theme.service.coffee +### + +taiga = @.taiga + + +class ThemeService extends taiga.Service = -> + use: (themeName) -> + stylesheetEl = $("link[rel='stylesheet']") + + if stylesheetEl.length == 0 + stylesheetEl = $("") + $("head").append(stylesheetEl) + + stylesheetEl.attr("href", "/styles/theme-#{themeName}.css") + + +angular.module("taigaCommon").service("tgThemeService", ThemeService) diff --git a/app/modules/services/theme.service.spec.coffee b/app/modules/services/theme.service.spec.coffee new file mode 100644 index 00000000..f2529504 --- /dev/null +++ b/app/modules/services/theme.service.spec.coffee @@ -0,0 +1,36 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: theme.service.spec.coffee +### + +describe "ThemeService", -> + themeService = null + data = { + theme: "testTheme" + } + + _inject = () -> + inject (_tgThemeService_) -> + themeService = _tgThemeService_ + + beforeEach -> + module "taigaCommon" + _inject() + + it "use a test theme", () -> + themeService.use(data.theme) + expect($("link[rel='stylesheet']")).to.have.attr("href", "/styles/theme-#{data.theme}.css") diff --git a/app/modules/services/user.service.coffee b/app/modules/services/user.service.coffee index f6c803d8..d9bdfbb4 100644 --- a/app/modules/services/user.service.coffee +++ b/app/modules/services/user.service.coffee @@ -1,9 +1,31 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user.service.coffee +### + taiga = @.taiga +bindMethods = taiga.bindMethods + class UserService extends taiga.Service @.$inject = ["tgResources"] constructor: (@rs) -> + bindMethods(@) getUserByUserName: (username) -> return @rs.users.getUserByUsername(username) @@ -11,6 +33,15 @@ class UserService extends taiga.Service getContacts: (userId) -> return @rs.users.getContacts(userId) + getLiked: (userId, pageNumber, objectType, textQuery) -> + return @rs.users.getLiked(userId, pageNumber, objectType, textQuery) + + getVoted: (userId, pageNumber, objectType, textQuery) -> + return @rs.users.getVoted(userId, pageNumber, objectType, textQuery) + + getWatched: (userId, pageNumber, objectType, textQuery) -> + return @rs.users.getWatched(userId, pageNumber, objectType, textQuery) + getStats: (userId) -> return @rs.users.getStats(userId) diff --git a/app/modules/services/user.service.spec.coffee b/app/modules/services/user.service.spec.coffee index eb8156a7..04629504 100644 --- a/app/modules/services/user.service.spec.coffee +++ b/app/modules/services/user.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user.service.spec.coffee +### + describe "UserService", -> userService = null $q = null @@ -32,19 +51,6 @@ describe "UserService", -> _mocks() _inject() - it "get user contacts", () -> - userId = 2 - - contacts = [ - {id: 1}, - {id: 2}, - {id: 3} - ] - - mocks.resources.users.getContacts.withArgs(userId).returns(true) - - expect(userService.getContacts(userId)).to.be.true - it "attach user contacts to projects", (done) -> userId = 2 @@ -88,6 +94,75 @@ describe "UserService", -> $rootScope.$apply() + it "get user liked", (done) -> + userId = 2 + pageNumber = 1 + objectType = null + textQuery = null + + liked = [ + {id: 1}, + {id: 2}, + {id: 3} + ] + + mocks.resources.users.getLiked = sinon.stub() + mocks.resources.users.getLiked.withArgs(userId, pageNumber, objectType, textQuery) + .promise() + .resolve(liked) + + userService.getLiked(userId, pageNumber, objectType, textQuery).then (_liked_) -> + expect(_liked_).to.be.eql(liked) + done() + + $rootScope.$apply() + + it "get user voted", (done) -> + userId = 2 + pageNumber = 1 + objectType = null + textQuery = null + + voted = [ + {id: 1}, + {id: 2}, + {id: 3} + ] + + mocks.resources.users.getVoted = sinon.stub() + mocks.resources.users.getVoted.withArgs(userId, pageNumber, objectType, textQuery) + .promise() + .resolve(voted) + + userService.getVoted(userId, pageNumber, objectType, textQuery).then (_voted_) -> + expect(_voted_).to.be.eql(voted) + done() + + $rootScope.$apply() + + it "get user watched", (done) -> + userId = 2 + pageNumber = 1 + objectType = null + textQuery = null + + watched = [ + {id: 1}, + {id: 2}, + {id: 3} + ] + + mocks.resources.users.getWatched = sinon.stub() + mocks.resources.users.getWatched.withArgs(userId, pageNumber, objectType, textQuery) + .promise() + .resolve(watched) + + userService.getWatched(userId, pageNumber, objectType, textQuery).then (_watched_) -> + expect(_watched_).to.be.eql(watched) + done() + + $rootScope.$apply() + it "get user by username", (done) -> username = "username-1" diff --git a/app/modules/services/xhrError.service.coffee b/app/modules/services/xhrError.service.coffee index 3f8f2e5b..86bbd3b7 100644 --- a/app/modules/services/xhrError.service.coffee +++ b/app/modules/services/xhrError.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: xhrError.service.coffee +### + class xhrError extends taiga.Service @.$inject = [ "$q", diff --git a/app/modules/services/xhrError.service.spec.coffee b/app/modules/services/xhrError.service.spec.coffee index 44f683e6..af393641 100644 --- a/app/modules/services/xhrError.service.spec.coffee +++ b/app/modules/services/xhrError.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: xhrError.service.spec.coffee +### + describe "tgXhrErrorService", -> xhrErrorService = provide = null mocks = {} diff --git a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment-image.jade b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment-image.jade index d82cc411..2d5424ae 100644 --- a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment-image.jade +++ b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment-image.jade @@ -1,5 +1,5 @@ // timeline-attachment directive div.activity-image-attachment blockquote - a(href="{{::attachment.url}}", title="See {{::attachment.filename}}", target="_blank") - img(ng-src="{{::attachment.thumb_url || attachment.url}}", alt="{{::attachment.filename}}") + a(href="{{::attachment.get('url')}}", title="See {{::attachment.get('filename')}}", target="_blank") + img(ng-src="{{::attachment.get('thumb_url') || attachment.get('url')}}", alt="{{::attachment.get('filename')}}") diff --git a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.coffee b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.coffee index 10e438a4..ad26327a 100644 --- a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.coffee +++ b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-attachment.directive.coffee +### + UserTimelineAttachmentDirective = (template, $compile) -> validFileExtensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"] @@ -8,7 +27,7 @@ UserTimelineAttachmentDirective = (template, $compile) -> return url.indexOf(extension, url - extension.length) != -1 link = (scope, el) -> - is_image = isImage(scope.attachment.url) + is_image = isImage(scope.attachment.get('url')) if is_image templateHtml = template.get("user-timeline/user-timeline-attachment/user-timeline-attachment-image.html") diff --git a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.spec.coffee b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.spec.coffee index 37a3f927..31d6171e 100644 --- a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.spec.coffee +++ b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.directive.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-attachment.directive.spec.coffee +### + describe "userTimelineAttachmentDirective", () -> element = scope = compile = provide = null mockTgTemplate = null @@ -32,9 +51,9 @@ describe "userTimelineAttachmentDirective", () -> compile = $compile it "attachment image template", () -> - scope.attachment = { + scope.attachment = Immutable.fromJS({ url: "path/path/file.jpg" - } + }) mockTgTemplate.get .withArgs("user-timeline/user-timeline-attachment/user-timeline-attachment-image.html") @@ -45,9 +64,9 @@ describe "userTimelineAttachmentDirective", () -> expect(elm.find('#image')).to.have.length(1) it "attachment file template", () -> - scope.attachment = { + scope.attachment = Immutable.fromJS({ url: "path/path/file.pdf" - } + }) mockTgTemplate.get .withArgs("user-timeline/user-timeline-attachment/user-timeline-attachment.html") diff --git a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.jade b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.jade index cdac755e..a2f83b04 100644 --- a/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.jade +++ b/app/modules/user-timeline/user-timeline-attachment/user-timeline-attachment.jade @@ -1,5 +1,5 @@ div.single-attachment blockquote - a(ng-href="{{ attachment.url }}", title="Click to download {{ attachment.filename }}", target="_blank") + a(ng-href="{{ attachment.get('url') }}", title="Click to download {{ attachment.get('filename') }}", target="_blank") span.icon.icon-document - span {{attachment.filename}} + span {{attachment.get('filename')}} diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee index f315fe7c..2d21c17b 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-item-title.service.coffee +### + unslugify = @.taiga.unslugify class UserTimelineItemTitle @@ -14,69 +33,93 @@ class UserTimelineItemTitle 'severity': 'ISSUES.FIELDS.SEVERITY', 'priority': 'ISSUES.FIELDS.PRIORITY', 'type': 'ISSUES.FIELDS.TYPE', - 'is_iocaine': 'TASK.FIELDS.IS_IOCAINE' + 'is_iocaine': 'TASK.FIELDS.IS_IOCAINE', + 'is_blocked': 'COMMON.FIELDS.IS_BLOCKED' + } + + _params: { + username: (timeline, event) -> + user = timeline.getIn(['data', 'user']) + + if user.get('is_profile_visible') + title_attr = @translate.instant('COMMON.SEE_USER_PROFILE', {username: user.get('username')}) + url = "user-profile:username=timeline.getIn(['data', 'user', 'username'])" + + return @._getLink(url, user.get('name'), title_attr) + else + return @._getUsernameSpan(user.get('name')) + + field_name: (timeline, event) -> + field_name = timeline.getIn(['data', 'value_diff', 'key']) + + return @translate.instant(@._fieldTranslationKey[field_name]) + + project_name: (timeline, event) -> + url = "project:project=timeline.getIn(['data', 'project', 'slug'])" + + return @._getLink(url, timeline.getIn(["data", "project", "name"])) + + new_value: (timeline, event) -> + if _.isArray(timeline.getIn(["data", "value_diff", "value"]).toJS()) + value = timeline.getIn(["data", "value_diff", "value"]).get(1) + + # assigned to unasigned + if value == null && timeline.getIn(["data", "value_diff", "key"]) == 'assigned_to' + value = @translate.instant('ACTIVITY.VALUES.UNASSIGNED') + + return value + else + return timeline.getIn(["data", "value_diff", "value"]).first().get(1) + + sprint_name: (timeline, event) -> + url = "project-taskboard:project=timeline.getIn(['data', 'project', 'slug']),sprint=timeline.getIn(['data', 'milestone', 'slug'])" + + return @._getLink(url, timeline.getIn(['data', 'milestone', 'name'])) + + us_name: (timeline, event) -> + obj = @._getTimelineObj(timeline, event).get('userstory') + + event_us = {obj: 'parent_userstory'} + url = @._getDetailObjUrl(event_us) + + text = '#' + obj.get('ref') + ' ' + obj.get('subject') + + return @._getLink(url, text) + + obj_name: (timeline, event) -> + obj = @._getTimelineObj(timeline, event) + url = @._getDetailObjUrl(event) + + if event.obj == 'wikipage' + text = unslugify(obj.get('slug')) + else if event.obj == 'milestone' + text = obj.get('name') + else + text = '#' + obj.get('ref') + ' ' + obj.get('subject') + + return @._getLink(url, text) + + role_name: (timeline, event) -> + return timeline.getIn(['data', 'value_diff', 'value']).keySeq().first() } constructor: (@translate) -> _translateTitleParams: (param, timeline, event) -> - if param == "username" - user = timeline.data.user - title_attr = @translate.instant('COMMON.SEE_USER_PROFILE', {username: user.username}) - url = 'user-profile:username=vm.activity.user.username' - - return @._getLink(url, user.name, title_attr) - - else if param == 'field_name' - field_name = Object.keys(timeline.data.values_diff)[0] - - return @translate.instant(@._fieldTranslationKey[field_name]) - - else if param == 'project_name' - url = 'project:project=vm.activity.project.slug' - - return @._getLink(url, timeline.data.project.name) - - else if param == 'sprint_name' - url = 'project-taskboard:project=vm.activity.project.slug,sprint=vm.activity.sprint.slug' - - return @._getLink(url, timeline.data.milestone.name) - - else if param == 'us_name' - obj = @._getTimelineObj(timeline, event).userstory - - event_us = {obj: 'parent_userstory'} - url = @._getDetailObjUrl(event_us) - - text = '#' + obj.ref + ' ' + obj.subject - - return @._getLink(url, text) - - else if param == 'obj_name' - obj = @._getTimelineObj(timeline, event) - url = @._getDetailObjUrl(event) - - if event.obj == 'wikipage' - text = unslugify(obj.slug) - else if event.obj == 'milestone' - text = obj.name - else - text = '#' + obj.ref + ' ' + obj.subject - - return @._getLink(url, text) + return @._params[param].call(this, timeline, event) _getTimelineObj: (timeline, event) -> - return timeline.data[event.obj] + return timeline.getIn(['data', event.obj]) _getDetailObjUrl: (event) -> url = { - "issue": ["project-issues-detail", ":project=vm.activity.project.slug,ref=vm.activity.obj.ref"], - "wikipage": ["project-wiki-page", ":project=vm.activity.project.slug,slug=vm.activity.obj.slug"], - "task": ["project-tasks-detail", ":project=vm.activity.project.slug,ref=vm.activity.obj.ref"], - "userstory": ["project-userstories-detail", ":project=vm.activity.project.slug,ref=vm.activity.obj.ref"], - "parent_userstory": ["project-userstories-detail", ":project=vm.activity.project.slug,ref=vm.activity.obj.userstory.ref"], - "milestone": ["project-taskboard", ":project=vm.activity.project.slug,sprint=vm.activity.obj.slug"] + "issue": ["project-issues-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"], + "wikipage": ["project-wiki-page", ":project=timeline.getIn(['data', 'project', 'slug']),slug=timeline.getIn(['obj', 'slug'])"], + "task": ["project-tasks-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"], + "userstory": ["project-userstories-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'ref'])"], + "parent_userstory": ["project-userstories-detail", ":project=timeline.getIn(['data', 'project', 'slug']),ref=timeline.getIn(['obj', 'userstory', 'ref'])"], + "milestone": ["project-taskboard", ":project=timeline.getIn(['data', 'project', 'slug']),sprint=timeline.getIn(['obj', 'slug'])"] } return url[event.obj][0] + url[event.obj][1] @@ -90,6 +133,14 @@ class UserTimelineItemTitle .attr('title', title) .prop('outerHTML') + _getUsernameSpan: (text) -> + title = title || text + + return $('') + .addClass('username') + .text(text) + .prop('outerHTML') + _getParams: (timeline, event, timeline_type) -> params = {} diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee index 5bc7949d..ef60ffce 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-title.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-item-title.service.spec.coffee +### + describe "tgUserTimelineItemTitle", -> mySvc = null mockTranslate = null @@ -32,14 +51,15 @@ describe "tgUserTimelineItemTitle", -> _setup() it "title with username", () -> - timeline = { + timeline = Immutable.fromJS({ data: { user: { username: 'xx', - name: 'oo' + name: 'oo', + is_profile_visible: true } } - } + }) event = {} @@ -49,11 +69,45 @@ describe "tgUserTimelineItemTitle", -> } mockTranslate.instant - .withArgs('COMMON.SEE_USER_PROFILE', {username: timeline.data.user.username}) + .withArgs('COMMON.SEE_USER_PROFILE', {username: timeline.getIn(['data', 'user', 'username'])}) .returns('user-param') usernamelink = sinon.match ((value) -> - return value.username == 'oo' + return value.username == 'oo' + ), "usernamelink" + + mockTranslate.instant + .withArgs('TITLE_USER_NAME', usernamelink) + .returns('title_ok') + + title = mySvc.getTitle(timeline, event, type) + + expect(title).to.be.equal("title_ok") + + it "title with username not visible", () -> + timeline = Immutable.fromJS({ + data: { + user: { + username: 'xx', + name: 'oo', + is_profile_visible: false + } + } + }) + + event = {} + + type = { + key: 'TITLE_USER_NAME', + translate_params: ['username'] + } + + mockTranslate.instant + .withArgs('COMMON.SEE_USER_PROFILE', {username: timeline.getIn(['data', 'user', 'username'])}) + .returns('user-param') + + usernamelink = sinon.match ((value) -> + return value.username == 'oo' ), "usernamelink" mockTranslate.instant @@ -65,13 +119,13 @@ describe "tgUserTimelineItemTitle", -> expect(title).to.be.equal("title_ok") it "title with a field name", () -> - timeline = { + timeline = Immutable.fromJS({ data: { - values_diff: { - status: {} + value_diff: { + key: 'status' } } - } + }) event = {} @@ -92,19 +146,43 @@ describe "tgUserTimelineItemTitle", -> .withArgs('TITLE_FIELD', fieldparam) .returns('title_ok') - title = mySvc.getTitle(timeline, event, type) expect(title).to.be.equal("title_ok") + it "title with new value", () -> + timeline = Immutable.fromJS({ + data: { + value_diff: { + key: 'status', + value: ['old', 'new'] + } + } + }) + + event = {} + + type = { + key: 'NEW_VALUE', + translate_params: ['new_value'] + } + + mockTranslate.instant + .withArgs('NEW_VALUE', {new_value: 'new'}) + .returns('new_value_ok') + + title = mySvc.getTitle(timeline, event, type) + + expect(title).to.be.equal("new_value_ok") + it "title with project name", () -> - timeline = { + timeline = Immutable.fromJS({ data: { project: { name: "project_name" } } - } + }) event = {} @@ -114,7 +192,7 @@ describe "tgUserTimelineItemTitle", -> } projectparam = sinon.match ((value) -> - return value.project_name == 'project_name' + return value.project_name == 'project_name' ), "projectparam" mockTranslate.instant @@ -126,13 +204,13 @@ describe "tgUserTimelineItemTitle", -> expect(title).to.be.equal("title_ok") it "title with sprint name", () -> - timeline = { + timeline = Immutable.fromJS({ data: { milestone: { name: "milestone_name" } } - } + }) event = {} @@ -142,7 +220,7 @@ describe "tgUserTimelineItemTitle", -> } milestoneparam = sinon.match ((value) -> - return value.sprint_name == 'milestone_name' + return value.sprint_name == 'milestone_name' ), "milestoneparam" mockTranslate.instant @@ -154,14 +232,14 @@ describe "tgUserTimelineItemTitle", -> expect(title).to.be.equal("title_ok") it "title with object", () -> - timeline = { + timeline = Immutable.fromJS({ data: { issue: { ref: '123', subject: 'subject' } } - } + }) event = { obj: 'issue', @@ -173,7 +251,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.obj_name == '#123 subject' + return value.obj_name == '#123 subject' ), "objparam" mockTranslate.instant @@ -185,13 +263,13 @@ describe "tgUserTimelineItemTitle", -> expect(title).to.be.equal("title_ok") it "title obj wiki", () -> - timeline = { + timeline = Immutable.fromJS({ data: { wikipage: { slug: 'slug-wiki', } } - } + }) event = { obj: 'wikipage', @@ -203,7 +281,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.obj_name == 'Slug wiki' + return value.obj_name == 'Slug wiki' ), "objparam" mockTranslate.instant @@ -215,13 +293,13 @@ describe "tgUserTimelineItemTitle", -> expect(title).to.be.equal("title_ok") it "title obj milestone", () -> - timeline = { + timeline = Immutable.fromJS({ data: { milestone: { name: 'milestone_name', } } - } + }) event = { obj: 'milestone', @@ -233,7 +311,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.obj_name == 'milestone_name' + return value.obj_name == 'milestone_name' ), "objparam" mockTranslate.instant @@ -245,7 +323,7 @@ describe "tgUserTimelineItemTitle", -> expect(title).to.be.equal("title_ok") it "task title with us_name", () -> - timeline = { + timeline = Immutable.fromJS({ data: { task: { name: 'task_name', @@ -255,7 +333,7 @@ describe "tgUserTimelineItemTitle", -> } } } - } + }) event = { obj: 'task', @@ -267,7 +345,7 @@ describe "tgUserTimelineItemTitle", -> } objparam = sinon.match ((value) -> - return value.us_name == '#2 subject' + return value.us_name == '#2 subject' ), "objparam" mockTranslate.instant diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee index 1b298aec..56fa4941 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-item-type.service.coffee +### + timelineType = (timeline, event) -> types = [ { # NewMember @@ -6,10 +25,10 @@ timelineType = (timeline, event) -> key: 'TIMELINE.NEW_MEMBER', translate_params: ['project_name'] member: (timeline) -> - return { - user: timeline.data.user, - role: timeline.data.role - } + return Immutable.Map({ + user: timeline.getIn(['data', 'user']), + role: timeline.getIn(['data', 'role']) + }) }, { # NewProject check: (timeline, event) -> @@ -17,11 +36,13 @@ timelineType = (timeline, event) -> key: 'TIMELINE.NEW_PROJECT', translate_params: ['username', 'project_name'], description: (timeline) -> - return timeline.data.project.description + return timeline.getIn(['data', 'project', 'description']) }, { # NewAttachment check: (timeline, event) -> - return event.type == 'change' && timeline.data.values_diff.attachments + return event.type == 'change' && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'attachments' key: 'TIMELINE.UPLOAD_ATTACHMENT', translate_params: ['username', 'obj_name'] }, @@ -45,13 +66,13 @@ timelineType = (timeline, event) -> }, { # NewTask check: (timeline, event) -> - return event.obj == 'task' && event.type == 'create' && !timeline.data.task.userstory + return event.obj == 'task' && event.type == 'create' && !timeline.getIn(['data', 'task', 'userstory']) key: 'TIMELINE.TASK_CREATED', translate_params: ['username', 'project_name', 'obj_name'] }, { # NewTask with US check: (timeline, event) -> - return event.obj == 'task' && event.type == 'create' && timeline.data.task.userstory + return event.obj == 'task' && event.type == 'create' && timeline.getIn(['data', 'task', 'userstory']) key: 'TIMELINE.TASK_CREATED_WITH_US', translate_params: ['username', 'project_name', 'obj_name', 'us_name'] }, @@ -63,64 +84,79 @@ timelineType = (timeline, event) -> }, { # NewUsComment check: (timeline, event) -> - return timeline.data.comment && event.obj == 'userstory' + return timeline.getIn(['data', 'comment']) && event.obj == 'userstory' key: 'TIMELINE.NEW_COMMENT_US', translate_params: ['username', 'obj_name'], description: (timeline) -> - return $(timeline.data.comment_html).text() + return $(timeline.getIn(['data', 'comment_html'])).text() }, { # NewIssueComment check: (timeline, event) -> - return timeline.data.comment && event.obj == 'issue' + return timeline.getIn(['data', 'comment']) && event.obj == 'issue' key: 'TIMELINE.NEW_COMMENT_ISSUE', translate_params: ['username', 'obj_name'], description: (timeline) -> - return $(timeline.data.comment_html).text() + return $(timeline.getIn(['data', 'comment_html'])).text() }, { # NewTaskComment check: (timeline, event) -> - return timeline.data.comment && event.obj == 'task' + return timeline.getIn(['data', 'comment']) && event.obj == 'task' key: 'TIMELINE.NEW_COMMENT_TASK' translate_params: ['username', 'obj_name'], description: (timeline) -> - return $(timeline.data.comment_html).text() + return $(timeline.getIn(['data', 'comment_html'])).text() }, - { # UsToMilestone - check: (timeline, event, field_name) -> - if field_name == 'milestone' && event.type == 'change' - return timeline.data.values_diff.milestone[0] == null - - return false - key: 'TIMELINE.US_ADDED_MILESTONE', - translate_params: ['username', 'obj_name', 'sprint_name'] + { # UsMove + check: (timeline, event) -> + return timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'moveInBacklog' && + timeline.hasIn(['data', 'value_diff', 'value', 'backlog_order']) && + event.type == 'change' + key: 'TIMELINE.US_MOVED', + translate_params: ['username', 'obj_name'] }, { # UsToBacklog - check: (timeline, event, field_name) -> - if field_name == 'milestone' && event.type == 'change' - return timeline.data.values_diff.milestone[1] == null + check: (timeline, event) -> + if timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'moveInBacklog' && + event.type == 'change' + + return timeline.getIn(['data', 'value_diff', 'value', 'milestone']).get(1) == null return false key: 'TIMELINE.US_REMOVED_FROM_MILESTONE', translate_params: ['username', 'obj_name'] }, + { # UsToMilestone + check: (timeline, event) -> + return timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'moveInBacklog' && + event.type == 'change' + key: 'TIMELINE.US_ADDED_MILESTONE', + translate_params: ['username', 'obj_name', 'sprint_name'] + }, { # Blocked check: (timeline, event) -> - if event.type == 'change' && timeline.data.values_diff.is_blocked - return timeline.data.values_diff.is_blocked[1] == true + if timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'blocked' && + event.type == 'change' + return timeline.getIn(['data', 'value_diff', 'value', 'is_blocked']).get(1) == true return false key: 'TIMELINE.BLOCKED', translate_params: ['username', 'obj_name'], description: (timeline) -> - if timeline.data.values_diff.blocked_note_html - return $(timeline.data.values_diff.blocked_note_html[1]).text() + if timeline.hasIn(['data', 'value_diff', 'value', 'blocked_note_html']) + return $(timeline.getIn(['data', 'value_diff', 'value', 'blocked_note_html']).get(1)).text() else return false }, { # UnBlocked check: (timeline, event) -> - if event.type == 'change' && timeline.data.values_diff.is_blocked - return timeline.data.values_diff.is_blocked[1] == false + if timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'blocked' && + event.type == 'change' + return timeline.getIn(['data', 'value_diff', 'value', 'is_blocked']).get(1) == false return false key: 'TIMELINE.UNBLOCKED', @@ -138,30 +174,83 @@ timelineType = (timeline, event) -> key: 'TIMELINE.WIKI_UPDATED', translate_params: ['username', 'obj_name'] }, - { # UsUpdated + { # UsUpdated points check: (timeline, event) -> - return event.obj == 'userstory' && event.type == 'change' + return event.obj == 'userstory' && + event.type == 'change' && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'points' + key: 'TIMELINE.US_UPDATED_POINTS', + translate_params: ['username', 'field_name', 'obj_name', 'new_value', 'role_name'] + }, + { # UsUpdated description + check: (timeline, event) -> + return event.obj == 'userstory' && + event.type == 'change' && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'description_diff' key: 'TIMELINE.US_UPDATED', translate_params: ['username', 'field_name', 'obj_name'] }, - { # IssueUpdated + { # UsUpdated general check: (timeline, event) -> - return event.obj == 'issue' && event.type == 'change' + return event.obj == 'userstory' && + event.type == 'change' + key: 'TIMELINE.US_UPDATED_WITH_NEW_VALUE', + translate_params: ['username', 'field_name', 'obj_name', 'new_value'] + }, + { # IssueUpdated description + check: (timeline, event) -> + return event.obj == 'issue' && + event.type == 'change' && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'description_diff' key: 'TIMELINE.ISSUE_UPDATED', translate_params: ['username', 'field_name', 'obj_name'] }, - { # TaskUpdated + { # IssueUpdated general check: (timeline, event) -> - return event.obj == 'task' && event.type == 'change' && !timeline.data.task.userstory + return event.obj == 'issue' && + event.type == 'change' + key: 'TIMELINE.ISSUE_UPDATED_WITH_NEW_VALUE', + translate_params: ['username', 'field_name', 'obj_name', 'new_value'] + }, + { # TaskUpdated description + check: (timeline, event) -> + return event.obj == 'task' && + event.type == 'change' && + !timeline.getIn('data', 'task', 'userstory') && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'description_diff' key: 'TIMELINE.TASK_UPDATED', translate_params: ['username', 'field_name', 'obj_name'] }, - { # TaskUpdated with US + { # TaskUpdated with US description check: (timeline, event) -> - return event.obj == 'task' && event.type == 'change' && timeline.data.task.userstory + return event.obj == 'task' && + event.type == 'change' && + timeline.getIn('data', 'task', 'userstory') && + timeline.hasIn(['data', 'value_diff']) && + timeline.getIn(['data', 'value_diff', 'key']) == 'description_diff' key: 'TIMELINE.TASK_UPDATED_WITH_US', translate_params: ['username', 'field_name', 'obj_name', 'us_name'] }, + { # TaskUpdated general + check: (timeline, event) -> + return event.obj == 'task' && + event.type == 'change' && + !timeline.getIn(['data', 'task', 'userstory']) + key: 'TIMELINE.TASK_UPDATED_WITH_NEW_VALUE', + translate_params: ['username', 'field_name', 'obj_name', 'new_value'] + }, + { # TaskUpdated with US + check: (timeline, event) -> + return event.obj == 'task' && + event.type == 'change' && + timeline.getIn(['data', 'task', 'userstory']) + key: 'TIMELINE.TASK_UPDATED_WITH_US_NEW_VALUE', + translate_params: ['username', 'field_name', 'obj_name', 'us_name', 'new_value'] + }, { # New User check: (timeline, event) -> return event.obj == 'user' && event.type == 'create' @@ -170,11 +259,8 @@ timelineType = (timeline, event) -> } ] - if timeline.data.values_diff - field_name = Object.keys(timeline.data.values_diff)[0] - return _.find types, (obj) -> - return obj.check(timeline, event, field_name) + return obj.check(timeline, event) class UserTimelineType getType: (timeline, event) -> timelineType(timeline, event) diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.spec.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.spec.coffee index 5198f07c..7ed49f79 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.spec.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item-type.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-item-type.service.spec.coffee +### + describe "tgUserTimelineItemType", -> mySvc = null diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.coffee deleted file mode 100644 index c1e9cde6..00000000 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.coffee +++ /dev/null @@ -1,45 +0,0 @@ -class UserTimelineItemController - @.$inject = [ - "tgUserTimelineItemType", - "tgUserTimelineItemTitle" - ] - - constructor: (@userTimelineItemType, @userTimelineItemTitle) -> - timeline = @.timeline.toJS() - - event = @.parseEventType(timeline.event_type) - type = @userTimelineItemType.getType(timeline, event) - - @.activity = {} - - @.activity.user = timeline.data.user - @.activity.project = timeline.data.project - @.activity.sprint = timeline.data.milestone - @.activity.title = @userTimelineItemTitle.getTitle(timeline, event, type) - @.activity.created_formated = moment(timeline.created).fromNow() - @.activity.obj = @.getObject(timeline, event) - - if type.description - @.activity.description = type.description(timeline) - - if type.member - @.activity.member = type.member(timeline) - - if timeline.data.values_diff?.attachments - @.activity.attachments = timeline.data.values_diff.attachments.new - - parseEventType: (event_type) -> - event_type = event_type.split(".") - - return { - section: event_type[0], - obj: event_type[1], - type: event_type[2] - } - - getObject: (timeline, event) -> - if timeline.data[event.obj] - return timeline.data[event.obj] - -angular.module("taigaUserTimeline") - .controller("UserTimelineItem", UserTimelineItemController) diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.spec.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.spec.coffee deleted file mode 100644 index fae5ad1a..00000000 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.controller.spec.coffee +++ /dev/null @@ -1,104 +0,0 @@ -describe "UserTimelineItemController", -> - controller = scope = provide = null - timeline = event = null - mockTgUserTimelineItemType = null - mockTgUserTimelineItemTitle = null - mockType = null - - _mockTgUserTimelineItemType = () -> - mockTgUserTimelineItemType = { - getType: sinon.stub() - } - - mockType = { - description: sinon.stub(), - member: sinon.stub() - } - - mockTgUserTimelineItemType.getType.withArgs(timeline).returns(mockType) - - provide.value "tgUserTimelineItemType", mockTgUserTimelineItemType - - _mockTgUserTimelineItemTitle = () -> - mockTgUserTimelineItemTitle = { - getTitle: sinon.stub() - } - - mockTgUserTimelineItemTitle.getTitle.withArgs(timeline, event, mockType).returns("fakeTitle") - - provide.value "tgUserTimelineItemTitle", mockTgUserTimelineItemTitle - - _mocks = () -> - module ($provide) -> - provide = $provide - _mockTgUserTimelineItemType() - _mockTgUserTimelineItemTitle() - - return null - - _setup = () -> - event = { - section: 'issues', - obj: 'issue', - type: 'created' - } - - timeline = { - event_type: 'issues.issue.created', - data: { - user: 'user_fake', - project: 'project_fake', - milestone: 'milestone_fake', - created: new Date().getTime(), - values_diff: {} - } - } - - scope = { - vm: { - timeline: timeline - } - } - - beforeEach -> - module "taigaUserTimeline" - - _setup() - _mocks() - - inject ($controller) -> - controller = $controller - - it "basic activity fields filled", () -> - timeline = scope.vm.timeline - timeline_immutable = Immutable.fromJS(timeline) - - myCtrl = controller("UserTimelineItem", {$scope: scope}, {timeline: timeline_immutable}) - - expect(myCtrl.activity.user).to.be.equal(timeline.data.user) - expect(myCtrl.activity.project).to.be.equal(timeline.data.project) - expect(myCtrl.activity.sprint).to.be.equal(timeline.data.milestone) - expect(myCtrl.activity.title).to.be.equal("fakeTitle") - expect(myCtrl.activity.created_formated).to.have.length.above(1) - - it "all activity fields filled", () -> - timeline = scope.vm.timeline - - attachment = "fakeAttachment" - timeline.data.values_diff.attachments = { - new: attachment - } - - description = "fakeDescription" - member = "fakeMember" - - mockType.description.withArgs(timeline).returns(description) - mockType.member.withArgs(timeline).returns(member) - - timeline_immutable = Immutable.fromJS(timeline) - - myCtrl = controller("UserTimelineItem", {$scope: scope}, {timeline: timeline_immutable}) - - expect(myCtrl.activity.description).to.be.equal(description) - expect(myCtrl.activity.member).to.be.equal(member) - expect(myCtrl.activity.attachments).to.be.equal(attachment) diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee b/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee index 15daf833..d0a18e2c 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item.directive.coffee @@ -1,8 +1,24 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-item.directive.coffee +### + UserTimelineItemDirective = () -> return { - controllerAs: "vm" - controller: "UserTimelineItem" - bindToController: true templateUrl: "user-timeline/user-timeline-item/user-timeline-item.html" scope: { timeline: "=tgUserTimelineItem" diff --git a/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade b/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade index b021cb68..e37c1dca 100644 --- a/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade +++ b/app/modules/user-timeline/user-timeline-item/user-timeline-item.jade @@ -1,22 +1,29 @@ div.activity-item - span.activity-date {{::vm.activity.created_formated}} + span.activity-date {{::timeline.get('created') | momentFromNow}} + + div.activity-info(tg-user-timeline-title="timeline") + div.activity-info - div.profile-contact-picture - a(tg-nav="user-profile:username=vm.activity.user.username", title="{{::vm.activity.user.name }}") - img(ng-src="{{::vm.activity.user.photo || '/images/unnamed.png'}}", alt="{{::vm.activity.user.name}}") + // profile image with url + div.profile-contact-picture(ng-if="timeline.getIn(['data', 'user', 'is_profile_visible'])") + a(tg-nav="user-profile:username=timeline.getIn(['data', 'user', 'username'])", title="{{::timeline.getIn(['data', 'user', 'name']) }}") + img(ng-src="{{::timeline.getIn(['data', 'user', 'photo']) || '/images/user-noimage.png'}}", alt="{{::timeline.getIn(['data', 'user', 'name'])}}") + // profile image without url + div.profile-contact-picture(ng-if="!timeline.getIn(['data', 'user', 'is_profile_visible'])") + img(ng-src="{{::timeline.getIn(['data', 'user', 'photo']) || '/images/user-noimage.png'}}", alt="{{::timeline.getIn(['data', 'user', 'name'])}}") - p(tg-compile-html="vm.activity.title") + p(tg-compile-html="timeline.get('title_html')") - blockquote.activity-comment-quote(ng-if="::vm.activity.description") - | {{::vm.activity.description | limitTo:300}} + blockquote.activity-comment-quote(ng-if="::timeline.get('description')") + | {{::timeline.get('description') | limitTo:300}} - .activity-member-view(ng-if="::vm.activity.member") - a.profile-member-picture(tg-nav="user-profile:username=vm.activity.member.user.username", title="{{::vm.activity.member.user.name }}") - img(ng-src="{{::vm.activity.member.user.photo}}", alt="{{::vm.activity.member.user.name}}") + .activity-member-view(ng-if="::timeline.has('member')") + a.profile-member-picture(tg-nav="user-profile:username=timeline.getIn(['member', 'user', 'username'])", title="{{::timeline.getIn(['member', 'user', 'name'])}}") + img(ng-src="{{::timeline.getIn(['member', 'user', 'photo'])}}", alt="{{::timeline.getIn(['member','user', 'name'])}}") .activity-member-info - a(tg-nav="user-profile:username=vm.activity.member.user.username", title="{{::vm.activity.member.user.name }}") - span {{::vm.activity.member.user.name}} - p {{::vm.activity.member.role.name}} + a(tg-nav="user-profile:username=timeline.getIn(['member', 'user', 'username'])", title="{{::timeline.getIn(['member','user', 'name'])}}") + span {{::timeline.getIn(['member','user', 'name'])}} + p {{::timeline.getIn(['member','role', 'name'])}} - div(ng-repeat="attachment in vm.activity.attachments") + div(tg-repeat="attachment in timeline.get('attachments')") div(tg-user-timeline-attachment="attachment") diff --git a/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.coffee b/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.coffee index 13bd45cd..5a6b314b 100644 --- a/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.coffee +++ b/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.coffee @@ -1,5 +1,26 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-pagination-sequence.service.coffee +### + UserTimelinePaginationSequence = () -> - return (config) -> + obj = {} + + obj.generate = (config) -> page = 1 items = Immutable.List() @@ -16,7 +37,10 @@ UserTimelinePaginationSequence = () -> data = response.get("data") if config.filter - data = config.filter(response.get("data")) + data = config.filter(data) + + if config.map + data = data.map(config.map) items = items.concat(data) @@ -32,4 +56,6 @@ UserTimelinePaginationSequence = () -> next: () -> next() } + return obj + angular.module("taigaUserTimeline").factory("tgUserTimelinePaginationSequenceService", UserTimelinePaginationSequence) diff --git a/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee b/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee index 5fa4937b..f1cf0207 100644 --- a/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee +++ b/app/modules/user-timeline/user-timeline-pagination-sequence/user-timeline-pagination-sequence.service.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline-pagination-sequence.service.spec.coffee +### + describe "tgUserTimelinePaginationSequenceService", -> userTimelinePaginationSequenceService = null @@ -35,7 +54,7 @@ describe "tgUserTimelinePaginationSequenceService", -> config.minItems = 10 - seq = userTimelinePaginationSequenceService(config) + seq = userTimelinePaginationSequenceService.generate(config) seq.next().then (result) -> result = result.toJS() @@ -66,7 +85,7 @@ describe "tgUserTimelinePaginationSequenceService", -> config.minItems = 10 - seq = userTimelinePaginationSequenceService(config) + seq = userTimelinePaginationSequenceService.generate(config) seq.next().then (result) -> result = result.toJS() @@ -97,7 +116,7 @@ describe "tgUserTimelinePaginationSequenceService", -> config.minItems = 2 - seq = userTimelinePaginationSequenceService(config) + seq = userTimelinePaginationSequenceService.generate(config) seq.next().then () -> seq.next().then (result) -> @@ -107,3 +126,31 @@ describe "tgUserTimelinePaginationSequenceService", -> expect(result.next).to.be.true done() + + + it "map items", (done) -> + config = {} + + page1 = Immutable.Map({ + next: false, + data: [1, 2, 3] + }) + + promise = sinon.stub() + promise.withArgs(1).promise().resolve(page1) + + config.fetch = (page) -> + return promise(page) + + config.minItems = 1 + + config.map = (item) => item + 1; + + seq = userTimelinePaginationSequenceService.generate(config) + + seq.next().then (result) -> + result = result.toJS() + + expect(result.items).to.be.eql([2, 3, 4]) + + done() diff --git a/app/modules/user-timeline/user-timeline.module.coffee b/app/modules/user-timeline/user-timeline.module.coffee index 728a1bbf..c5eef5c7 100644 --- a/app/modules/user-timeline/user-timeline.module.coffee +++ b/app/modules/user-timeline/user-timeline.module.coffee @@ -1 +1,20 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline.module.coffee +### + angular.module("taigaUserTimeline", []) diff --git a/app/modules/user-timeline/user-timeline/user-timeline.controller.coffee b/app/modules/user-timeline/user-timeline/user-timeline.controller.coffee index bd8d0840..5cb5c7e5 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.controller.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.controller.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as diff --git a/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee b/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee index c78ea0b4..2f78a968 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.controller.spec.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline.controller.spec.coffee +### + describe "UserTimelineController", -> controller = scope = $q = provide = $rootScope = null diff --git a/app/modules/user-timeline/user-timeline/user-timeline.directive.coffee b/app/modules/user-timeline/user-timeline/user-timeline.directive.coffee index 5b9314e8..8973e43f 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.directive.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.directive.coffee @@ -1,3 +1,22 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline.directive.coffee +### + UserTimelineDirective = -> return { templateUrl: "user-timeline/user-timeline/user-timeline.html", diff --git a/app/modules/user-timeline/user-timeline/user-timeline.scss b/app/modules/user-timeline/user-timeline/user-timeline.scss index 4f93fe7b..fc41472a 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.scss +++ b/app/modules/user-timeline/user-timeline/user-timeline.scss @@ -1,20 +1,22 @@ .profile-timeline { + border-top: 1px solid $whitish; .activity-item { border-bottom: 1px solid $whitish; - padding: 1rem .5rem; + padding: 1rem .5rem 1rem 0; position: relative; p { margin-bottom: 0; } - a { - color: $green-taiga; + a, + .username { + color: $primary; &:first-child { @extend %bold; color: $gray; } - &:hover { - color: $fresh-taiga; - } + } + a:hover { + color: $primary-light; } blockquote { line-height: 1.4rem; @@ -28,7 +30,6 @@ img { max-height: 640px; max-width: 640px; - width: 100%; } .activity-info { align-items: center; diff --git a/app/modules/user-timeline/user-timeline/user-timeline.service.coffee b/app/modules/user-timeline/user-timeline/user-timeline.service.coffee index 83e90752..b8e9b4c9 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.service.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.service.coffee @@ -1,23 +1,67 @@ +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline.service.coffee +### + taiga = @.taiga class UserTimelineService extends taiga.Service - @.$inject = ["tgResources", "tgUserTimelinePaginationSequenceService"] + @.$inject = [ + "tgResources", + "tgUserTimelinePaginationSequenceService", + "tgUserTimelineItemType", + "tgUserTimelineItemTitle" + ] - constructor: (@rs, @userTimelinePaginationSequenceService) -> + constructor: (@rs, @userTimelinePaginationSequenceService, @userTimelineItemType, @userTimelineItemTitle) -> + + _valid_fields: [ + 'status', + 'subject', + 'description_diff', + 'assigned_to', + 'points', + 'severity', + 'priority', + 'type', + 'attachments', + 'is_iocaine', + 'content_diff', + 'name', + 'estimated_finish', + 'estimated_start', + # customs + 'blocked', + 'moveInBacklog', + 'milestone' + ] _invalid: [ {# Items with only invalid fields check: (timeline) -> - values_diff = timeline.get("data").get("values_diff") + value_diff = timeline.get("data").get("value_diff") - if values_diff - values = Object.keys(values_diff.toJS()) + if value_diff + fieldKey = value_diff.get('key') - if values && values.length - if _.every(values, (value) => @._valid_fields.indexOf(value) == -1) + if @._valid_fields.indexOf(fieldKey) == -1 return true - else if values[0] == 'attachments' && - values_diff.get('attachments').get('new').size == 0 + else if fieldKey == 'attachments' && + value_diff.get('value').get('new').size == 0 return true return false @@ -39,68 +83,137 @@ class UserTimelineService extends taiga.Service {# Task milestone check: (timeline) -> event = timeline.get('event_type').split(".") + value_diff = timeline.get("data").get("value_diff") - if event[1] == "task" && event[2] == "change" - return timeline.get("data").get("values_diff").get("milestone") + if value_diff && + event[1] == "task" && + event[2] == "change" && + value_diff.get("key") == "milestone" + return timeline.get("data").get("value_diff").get("value") return false } ] - _valid_fields: [ - 'status', - 'subject', - 'description_diff', - 'assigned_to', - 'points', - 'severity', - 'priority', - 'type', - 'attachments', - 'milestone', - 'is_blocked', - 'is_iocaine', - 'content_diff', - 'name', - 'estimated_finish', - 'estimated_start' - ] - _isInValidTimeline: (timeline) -> return _.some @._invalid, (invalid) => return invalid.check.call(this, timeline) - getProfileTimeline: (userId, page) -> + _parseEventType: (event_type) -> + event_type = event_type.split(".") + + return { + section: event_type[0], + obj: event_type[1], + type: event_type[2] + } + + _getTimelineObject: (timeline, event) -> + if timeline.get('data').get(event.obj) + return timeline.get('data').get(event.obj) + + _attachExtraInfoToTimelineEntry: (timeline, event, type) -> + title = @userTimelineItemTitle.getTitle(timeline, event, type) + + timeline = timeline.set('title_html', title) + + timeline = timeline.set('obj', @._getTimelineObject(timeline, event)) + + if type.description + timeline = timeline.set('description', type.description(timeline)) + + if type.member + timeline = timeline.set('member', type.member(timeline)) + + if timeline.getIn(['data', 'value_diff', 'key']) == 'attachments' && + timeline.hasIn(['data', 'value_diff', 'value', 'new']) + timeline = timeline.set('attachments', timeline.getIn(['data', 'value_diff', 'value', 'new'])) + + return timeline + + # - create a entry per every item in the values_diff + _parseTimeline: (response) -> + newdata = Immutable.List() + + response.get('data').forEach (item) => + event = @._parseEventType(item.get('event_type')) + + data = item.get('data') + values_diff = data.get('values_diff') + + if values_diff && values_diff.count() + # blocked/unblocked change must be a single change + if values_diff.has('is_blocked') + values_diff = Immutable.Map({'blocked': values_diff}) + + if values_diff.has('milestone') + values_diff = Immutable.Map({'moveInBacklog': values_diff}) + else if event.obj == 'milestone' + values_diff = Immutable.Map({'milestone': values_diff}) + + values_diff.forEach (value, key) => + obj = Immutable.Map({ + key: key, + value: value + }) + + newItem = item.setIn(['data', 'value_diff'], obj) + newItem = newItem.deleteIn(['data', 'values_diff']) + newdata = newdata.push(newItem) + else + newItem = item.deleteIn(['data', 'values_diff']) + newdata = newdata.push(newItem) + + return response.set('data', newdata) + + _addEntyAttributes: (item) -> + event = @._parseEventType(item.get('event_type')) + type = @userTimelineItemType.getType(item, event) + + return @._attachExtraInfoToTimelineEntry(item, event, type) + + getProfileTimeline: (userId) -> config = {} config.fetch = (page) => return @rs.users.getProfileTimeline(userId, page) + .then (response) => + return @._parseTimeline(response) + + config.map = (obj) => @._addEntyAttributes(obj) config.filter = (items) => return items.filterNot (item) => @._isInValidTimeline(item) - return @userTimelinePaginationSequenceService(config) + return @userTimelinePaginationSequenceService.generate(config) getUserTimeline: (userId) -> config = {} config.fetch = (page) => return @rs.users.getUserTimeline(userId, page) + .then (response) => + return @._parseTimeline(response) + + config.map = (obj) => @._addEntyAttributes(obj) config.filter = (items) => return items.filterNot (item) => @._isInValidTimeline(item) - return @userTimelinePaginationSequenceService(config) + return @userTimelinePaginationSequenceService.generate(config) getProjectTimeline: (projectId) -> config = {} config.fetch = (page) => return @rs.projects.getTimeline(projectId, page) + .then (response) => return @._parseTimeline(response) + + config.map = (obj) => @._addEntyAttributes(obj) config.filter = (items) => return items.filterNot (item) => @._isInValidTimeline(item) - return @userTimelinePaginationSequenceService(config) + return @userTimelinePaginationSequenceService.generate(config) angular.module("taigaUserTimeline").service("tgUserTimelineService", UserTimelineService) diff --git a/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee b/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee index 8dbe3c6d..00000c1e 100644 --- a/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee +++ b/app/modules/user-timeline/user-timeline/user-timeline.service.spec.coffee @@ -1,7 +1,24 @@ -describe.skip "tgUserTimelineService", -> +### +# Copyright (C) 2014-2015 Taiga Agile LLC +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: user-timeline.service.spec.coffee +### + +describe "tgUserTimelineService", -> provide = null - $q = null - $rootScope = null userTimelineService = null mocks = {} @@ -9,7 +26,9 @@ describe.skip "tgUserTimelineService", -> mocks.resources = {} mocks.resources.users = { - getTimeline: sinon.stub() + getTimeline: sinon.stub(), + getProfileTimeline: sinon.stub(), + getUserTimeline: sinon.stub() } mocks.resources.projects = { @@ -23,10 +42,34 @@ describe.skip "tgUserTimelineService", -> provide.value "tgUserTimelinePaginationSequenceService", mocks.userTimelinePaginationSequence + _mockTgUserTimelineItemType = () -> + mocks.userTimelineItemType = { + getType: sinon.stub() + } + + mocks.getType = { + description: sinon.stub(), + member: sinon.stub() + } + + mocks.userTimelineItemType.getType.returns(mocks.getType) + + provide.value "tgUserTimelineItemType", mocks.userTimelineItemType + + _mockTgUserTimelineItemTitle = () -> + mocks.userTimelineItemTitle = { + getTitle: sinon.stub() + } + + provide.value "tgUserTimelineItemTitle", mocks.userTimelineItemTitle + _mocks = () -> module ($provide) -> provide = $provide + _mockResources() _mockUserTimelinePaginationSequence() + _mockTgUserTimelineItemType() + _mockTgUserTimelineItemTitle() return null @@ -34,10 +77,8 @@ describe.skip "tgUserTimelineService", -> _mocks() _inject = (callback) -> - inject (_tgUserTimelineService_, _$q_, _$rootScope_) -> + inject (_tgUserTimelineService_) -> userTimelineService = _tgUserTimelineService_ - $q = _$q_ - $rootScope = _$rootScope_ callback() if callback beforeEach -> @@ -75,7 +116,6 @@ describe.skip "tgUserTimelineService", -> event_type: "xx.tt.create", data: { values_diff: { - "fake2": "xx", "milestone": "xx" } } @@ -146,79 +186,102 @@ describe.skip "tgUserTimelineService", -> } ] - it "filter invalid profile timeline items", (done) -> + it "filter invalid profile timeline items", () -> userId = 3 page = 2 - mocks.resources.users.getProfileTimeline = (_userId_, _page_) -> - expect(_userId_).to.be.equal(userId) - expect(_page_).to.be.equal(page) + response = Immutable.fromJS({ + data: valid_items + }) - return $q (resolve, reject) -> - resolve(Immutable.fromJS(valid_items)) + mocks.resources.users.getProfileTimeline.withArgs(userId).promise().resolve(response) + mocks.userTimelinePaginationSequence.generate = (config) -> + return config.fetch().then (res) -> + expect(res.get('data').size).to.be.equal(13) - .then (_items_) -> - items = _items_.toJS() + items = config.filter(res.get('data')) + expect(items.size).to.be.equal(6) - expect(items).to.have.length(4) - expect(items[0]).to.be.eql(valid_items[0]) - expect(items[1]).to.be.eql(valid_items[3]) - expect(items[2]).to.be.eql(valid_items[5]) - expect(items[3]).to.be.eql(valid_items[9]) - - done() + return true result = userTimelineService.getProfileTimeline(userId) - mocks.userTimelinePaginationSequence.withArgs() + return expect(result).to.be.eventually.true - - it "filter invalid user timeline items", (done) -> + it "filter invalid user timeline items", () -> userId = 3 page = 2 - mocks.resources.users.getUserTimeline = (_userId_, _page_) -> - expect(_userId_).to.be.equal(userId) - expect(_page_).to.be.equal(page) + response = Immutable.fromJS({ + data: valid_items + }) - return $q (resolve, reject) -> - resolve(Immutable.fromJS(valid_items)) + mocks.resources.users.getUserTimeline.withArgs(userId).promise().resolve(response) - userTimelineService.getUserTimeline(userId, page) - .then (_items_) -> - items = _items_.toJS() + mocks.userTimelinePaginationSequence.generate = (config) -> + return config.fetch().then (res) -> + expect(res.get('data').size).to.be.equal(13) - expect(items).to.have.length(4) - expect(items[0]).to.be.eql(valid_items[0]) - expect(items[1]).to.be.eql(valid_items[3]) - expect(items[2]).to.be.eql(valid_items[5]) - expect(items[3]).to.be.eql(valid_items[9]) + items = config.filter(res.get('data')) + expect(items.size).to.be.equal(6) - done() + return true - $rootScope.$apply() + result = userTimelineService.getUserTimeline(userId) - it "filter invalid project timeline items", (done) -> - projectId = 3 + return expect(result).to.be.eventually.true + + it "filter invalid project timeline items", () -> + userId = 3 page = 2 - mocks.resources.projects.getTimeline = (_projectId_, _page_) -> - expect(_projectId_).to.be.equal(projectId) - expect(_page_).to.be.equal(page) + response = Immutable.fromJS({ + data: valid_items + }) - return $q (resolve, reject) -> - resolve(Immutable.fromJS(valid_items)) + mocks.resources.projects.getTimeline.withArgs(userId).promise().resolve(response) - userTimelineService.getProjectTimeline(projectId, page) - .then (_items_) -> - items = _items_.toJS() + mocks.userTimelinePaginationSequence.generate = (config) -> + return config.fetch().then (res) -> + expect(res.get('data').size).to.be.equal(13) - expect(items).to.have.length(4) - expect(items[0]).to.be.eql(valid_items[0]) - expect(items[1]).to.be.eql(valid_items[3]) - expect(items[2]).to.be.eql(valid_items[5]) - expect(items[3]).to.be.eql(valid_items[9]) - done() + items = config.filter(res.get('data')) + expect(items.size).to.be.equal(6) - $rootScope.$apply() + return true + + result = userTimelineService.getProjectTimeline(userId) + expect(result).to.be.eventually.true + + it "all timeline extra fields filled", () -> + timeline = Immutable.fromJS({ + event_type: 'issues.issue.created', + data: { + user: 'user_fake', + project: 'project_fake', + milestone: 'milestone_fake', + created: new Date().getTime(), + issue: { + id: 2 + }, + value_diff: { + key: 'attachments', + value: { + new: "fakeAttachment" + } + } + } + }) + + mocks.userTimelineItemTitle.getTitle.returns("fakeTitle") + mocks.getType.description.returns("fakeDescription") + mocks.getType.member.returns("fakeMember") + + timelineEntry = userTimelineService._addEntyAttributes(timeline) + + expect(timelineEntry.get('title_html')).to.be.equal("fakeTitle") + expect(timelineEntry.get('obj')).to.be.equal(timelineEntry.getIn(["data", "issue"])) + expect(timelineEntry.get("description")).to.be.equal("fakeDescription") + expect(timelineEntry.get("member")).to.be.equal("fakeMember") + expect(timelineEntry.get("attachments")).to.be.equal("fakeAttachment") diff --git a/app/partials/admin/admin-project-modules.jade b/app/partials/admin/admin-project-modules.jade index 5cb45cd3..ab4ade35 100644 --- a/app/partials/admin/admin-project-modules.jade +++ b/app/partials/admin/admin-project-modules.jade @@ -15,7 +15,7 @@ div.wrapper(tg-project-modules, ng-controller="ProjectProfileController as ctrl" form div.functionality(ng-class="{true:'active', false:''}[project.is_backlog_activated]") - div.icon.icon-backlog + div.icon.icon-scrum div.desc p span.title(translate="ADMIN.MODULES.BACKLOG") @@ -86,8 +86,16 @@ div.wrapper(tg-project-modules, ng-controller="ProjectProfileController as ctrl" div.videoconference-attributes.hidden select(ng-model="project.videoconferences", - ng-options="e.id as e.name for e in [{'id':'appear-in', 'name':'AppearIn'},{'id':'jitsi', 'name': 'Jitsi'},{'id':'talky', 'name': 'Talky'}]") + ng-options="e.id as e.name|translate for e in [{'id':'appear-in', 'name':'ADMIN.MODULES.APPEARIN_CHAT_ROOM'},{'id':'jitsi', 'name': 'ADMIN.MODULES.JITSI_CHAT_ROOM'},{'id':'talky', 'name': 'ADMIN.MODULES.TALKY_CHAT_ROOM'},{'id':'custom', 'name': 'ADMIN.MODULES.CUSTOM_CHAT_ROOM'}]") option(value="", translate="ADMIN.MODULES.SELECT_VIDEOCONFERENCE") - input(type="text", ng-model="project.videoconferences_salt", + input(ng-if="project.videoconferences && project.videoconferences != 'custom'", + type="text", + ng-model="project.videoconferences_extra_data", + data-maxlength="255", placeholder="{{'ADMIN.MODULES.SALT_CHAT_ROOM' | translate}}") + input(ng-if="project.videoconferences == 'custom'", + type="text", + ng-model="project.videoconferences_extra_data", + data-maxlength="255", + placeholder="{{'ADMIN.MODULES.URL_CHAT_ROOM' | translate}}") button.button-green.submit-button(type="submit", title="{{'COMMON.SAVE' | translate}}", translate="COMMON.SAVE") diff --git a/app/partials/admin/admin-project-profile.jade b/app/partials/admin/admin-project-profile.jade index 24f58587..aebc5d45 100644 --- a/app/partials/admin/admin-project-profile.jade +++ b/app/partials/admin/admin-project-profile.jade @@ -29,7 +29,7 @@ div.wrapper(tg-project-profile, ng-controller="ProjectProfileController as ctrl" 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", data-required="true") + data-type="digits") fieldset label(for="tags", translate="ADMIN.PROJECT_PROFILE.TAGS") diff --git a/app/partials/admin/admin-roles.jade b/app/partials/admin/admin-roles.jade index 6a383e16..2af066c0 100644 --- a/app/partials/admin/admin-roles.jade +++ b/app/partials/admin/admin-roles.jade @@ -22,7 +22,7 @@ div.wrapper.roles(ng-controller="RolesController as ctrl", div(tg-edit-role) .edit-role input(type="text", value="{{ role.name }}") - a.save.icon.icon-floppy(href="", title="{{'COMMON.SAVE' | translate}}") + a.save.icon-floppy(href="", title="{{'COMMON.SAVE' | translate}}") p.total span.role-name(title="{{'ADMIN.ROLES.COUNT_MEMBERS' | translate}}") {{ role.name }} @@ -33,7 +33,7 @@ div.wrapper.roles(ng-controller="RolesController as ctrl", div.general-category span(translate="ADMIN.ROLES.HELP_ROLE_ENABLED") div.check - input(type="checkbox", ng-model="role.computable", ng-change="ctrl.setComputable()") + input(type="checkbox", ng-model="role.computable", ng-change="ctrl.toggleComputable()") div span.check-text.check-yes(translate="COMMON.YES") span.check-text.check-no(translate="COMMON.NO") diff --git a/app/partials/admin/memberships-row-avatar.jade b/app/partials/admin/memberships-row-avatar.jade index 97491394..838f44cc 100644 --- a/app/partials/admin/memberships-row-avatar.jade +++ b/app/partials/admin/memberships-row-avatar.jade @@ -2,4 +2,6 @@ figure.avatar img(src!="<%- imgurl %>", alt!="<%- full_name %>") figcaption span.name <%- full_name %> - span.email <%- email %> + div + span.pending <%- pending %> + span.email <%- email %> diff --git a/app/partials/attachment/attachments.jade b/app/partials/attachment/attachments.jade index 0cd720a8..25ada019 100644 --- a/app/partials/attachment/attachments.jade +++ b/app/partials/attachment/attachments.jade @@ -26,3 +26,5 @@ section.attachments span.text(data-type="show", translate="ATTACHMENT.SHOW_DEPRECATED") span.text.hidden(data-type="hide", translate="ATTACHMENT.HIDE_DEPRECATED") span.more-attachments-num(translate="ATTACHMENT.COUNT_DEPRECATED", translate-values="{counter: '{{ctrl.deprecatedAttachmentsCount}}'}") + + div.lightbox.lightbox-block(tg-lb-attachment-preview) \ No newline at end of file diff --git a/app/partials/auth/login.jade b/app/partials/auth/login.jade index fadbd2f2..4cb2950d 100644 --- a/app/partials/auth/login.jade +++ b/app/partials/auth/login.jade @@ -1,6 +1,8 @@ doctype html -include ../includes/components/beta +div + include ../includes/components/beta + div.wrapper div.login-main div.login-container diff --git a/app/partials/backlog/backlog.jade b/app/partials/backlog/backlog.jade index 8741c465..ad6b8ffc 100644 --- a/app/partials/backlog/backlog.jade +++ b/app/partials/backlog/backlog.jade @@ -7,46 +7,65 @@ div.wrapper(tg-backlog, ng-controller="BacklogController as ctrl", include ../includes/modules/backlog-filters section.main.backlog include ../includes/components/mainTitle - include ../includes/components/summary - div.graphics-container.burndown-container - div.burndown(tg-burndown-backlog-graph) - include ../includes/modules/burndown + div.backlog-summary(tg-toggle-burndown-visibility) + include ../includes/components/summary + + div.empty-burndown(ng-if="showGraphPlaceholder") + div.graph-icon + include ../../svg/graph.svg + div.empty-text + p.title(translate="BACKLOG.CUSTOMIZE_GRAPH") + p {{'BACKLOG.CUSTOMIZE_GRAPH_TEXT' | translate}} #[a(href="", tg-nav="project-admin-project-profile-details:project=project.slug", title="{{'BACKLOG.CUSTOMIZE_GRAPH_TITLE' | translate}}") {{'BACKLOG.CUSTOMIZE_GRAPH_ADMIN' | translate}}] + + div.graphics-container.js-burndown-graph + div.burndown(tg-burndown-backlog-graph) + include ../includes/modules/burndown div.backlog-menu div.backlog-table-options - a.trans-button.move-to-current-sprint(href="", - title="{{'BACKLOG.MOVE_US_TO_CURRENT_SPRINT' | translate}}", - id="move-to-current-sprint") + a.trans-button.move-to-current-sprint( + href="" + title="{{'BACKLOG.MOVE_US_TO_CURRENT_SPRINT' | translate}}" + id="move-to-current-sprint" + ) span.icon.icon-move span.text(translate="BACKLOG.MOVE_US_TO_CURRENT_SPRINT") - a.trans-button(href="", - title="{{'BACKLOG.FILTERS.TOGGLE' | translate}}", - id="show-filters-button") - span.icon.icon-filter - span.text(translate="BACKLOG.FILTERS.SHOW") - a.trans-button(href="", - title="{{'BACKLOG.TAGS.TOGGLE' | translate}}", - id="show-tags") - span.icon.icon-tag - span.text(translate="BACKLOG.TAGS.SHOW") + a.trans-button( + ng-if="userstories.length" + href="" + title="{{'BACKLOG.FILTERS.TOGGLE' | translate}}" + id="show-filters-button" + translate="BACKLOG.FILTERS.SHOW" + ) + a.trans-button( + ng-if="userstories.length" + href="" + title="{{'BACKLOG.TAGS.TOGGLE' | translate}}" + id="show-tags" + translate="BACKLOG.TAGS.SHOW" + ) include ../includes/components/addnewus - section.backlog-table(ng-class="{'hidden': !visibleUserstories.length}") + section.backlog-table(ng-class="{'hidden': !userstories.length}") include ../includes/modules/backlog-table - div.empty.empty-backlog(ng-class="{'hidden': visibleUserstories.length}", tg-backlog-empty-sortable) - span.icon.icon-backlog - span.title(translate="BACKLOG.EMPTY") - a(href="", title="{{'BACKLOG.CREATE_NEW_US' | translate}}", - ng-click="ctrl.addNewUs('standard')", - tg-check-permission="add_us", - translate="BACKLOG.CREATE_NEW_US_EMPTY_HELP") + div.empty-backlog(ng-class="{'hidden': userstories.length}", tg-backlog-empty-sortable) + img( + src="/images/backlog-empty.png" + alt="{{'BACKLOG.EMPTY' | translate}}" + ) + p.title(translate="BACKLOG.EMPTY") + a(href="", title="{{'BACKLOG.CREATE_NEW_US' | translate}}" + ng-click="ctrl.addNewUs('standard')" + tg-check-permission="add_us" + translate="BACKLOG.CREATE_NEW_US_EMPTY_HELP" + ) sidebar.menu-secondary.sidebar include ../includes/modules/sprints - div.lightbox.lightbox-generic-form(tg-lb-create-edit-userstory) + div.lightbox.lightbox-generic-form.lb-create-edit-userstory(tg-lb-create-edit-userstory) include ../includes/modules/lightbox-us-create-edit div.lightbox.lightbox-generic-bulk(tg-lb-create-bulk-userstories) diff --git a/app/partials/backlog/filters.jade b/app/partials/backlog/filters.jade index 05faf672..1d504108 100644 --- a/app/partials/backlog/filters.jade +++ b/app/partials/backlog/filters.jade @@ -3,11 +3,15 @@ a.single-filter.active(data-type!="<%- f.type %>", data-id!="<%- f.id %>") span.name(style!="<%- f.style %>") | <%- f.name %> + <% if (f.count){ %> span.number <%- f.count %> + <% } %> <% } else { %> a.single-filter(data-type!="<%- f.type %>", data-id!="<%- f.id %>") span.name(style!="<%- f.style %>") | <%- f.name %> + <% if (f.count){ %> span.number <%- f.count %> + <% } %> <% } %> <% }) %> \ No newline at end of file diff --git a/app/partials/common/components/assigned-to.jade b/app/partials/common/components/assigned-to.jade index ee144283..7ad68ab3 100644 --- a/app/partials/common/components/assigned-to.jade +++ b/app/partials/common/components/assigned-to.jade @@ -9,14 +9,14 @@ a(href="" title="{{ 'COMMON.ASSIGNED_TO.TITLE_ACTION_EDIT_ASSIGNMENT'|translate }}", class!="user-assigned <% if(isEditable){ %>editable<% }; %>") span.assigned-name - <% if (assignedTo) { %> - <%- assignedTo.full_name_display %> - <% } else { %> - | {{ 'COMMON.ASSIGNED_TO.NOT_ASSIGNED'|translate }} - <% } %> - <% if(isEditable){ %> - span.icon.icon-arrow-bottom - <% }; %> + <% if (assignedTo) { %> + <%- assignedTo.full_name_display %> + <% } else { %> + | {{ 'COMMON.ASSIGNED_TO.NOT_ASSIGNED'|translate }} + <% } %> + <% if(isEditable){ %> + span.icon.icon-arrow-bottom + <% }; %> <% if (assignedTo!==null && isEditable) { %> a.icon.icon-delete(href="" title="{{'COMMON.ASSIGNED_TO.DELETE_ASSIGNMENT' | translate}}") <% } %> diff --git a/app/partials/common/components/created-by.jade b/app/partials/common/components/created-by.jade index e9f74f06..03ed8980 100644 --- a/app/partials/common/components/created-by.jade +++ b/app/partials/common/components/created-by.jade @@ -1,7 +1,9 @@ .user-avatar - img(src!="<%- owner.photo %>", alt!="<%- owner.full_name_display %>") + a(href!="<%- url %>", title!="<%- owner.full_name_display %>") + img(src!="<%- owner.photo %>", alt!="<%- owner.full_name_display %>") .created-by - span.created-title(translate="COMMON.CREATED_BY", translate-values!="{ 'fullDisplayName': '<%- owner.full_name_display %>'}") + a(href!="<%- url %>", title!="<%- owner.full_name_display %>") + span.created-title(translate="COMMON.CREATED_BY", translate-values!="{ 'fullDisplayName': '<%- owner.full_name_display %>'}") span.created-date | <%- date %> diff --git a/app/partials/common/components/delete-button.jade b/app/partials/common/components/delete-button.jade index 205e4fb4..cc325950 100644 --- a/app/partials/common/components/delete-button.jade +++ b/app/partials/common/components/delete-button.jade @@ -1,2 +1,2 @@ -a(href="", class="button button-red") +a.button-red.button-delete(href="") span(translate="COMMON.DELETE") diff --git a/app/partials/common/components/kanban-placeholder.jade b/app/partials/common/components/kanban-placeholder.jade new file mode 100644 index 00000000..551fcbe8 --- /dev/null +++ b/app/partials/common/components/kanban-placeholder.jade @@ -0,0 +1,7 @@ +.placeholder-avatar + .image + .text + .line + .line +p.title {{'KANBAN.PLACEHOLDER_CARD_TITLE' | translate}} +p {{'KANBAN.PLACEHOLDER_CARD_TEXT' | translate}} diff --git a/app/partials/common/components/status-display.jade b/app/partials/common/components/status-display.jade index 1062f3f4..098ffa32 100644 --- a/app/partials/common/components/status-display.jade +++ b/app/partials/common/components/status-display.jade @@ -3,5 +3,5 @@ span(translate="COMMON.STATUS.CLOSED") <% } else { %> span(translate="COMMON.STATUS.OPEN") <% } %> -span(class="us-detail-status", style!="color:<%- status.color %>") +span(class="detail-status", style!="color:<%- status.color %>") | <%- status.name %> diff --git a/app/partials/common/components/taskboard-placeholder.jade b/app/partials/common/components/taskboard-placeholder.jade new file mode 100644 index 00000000..89af6d87 --- /dev/null +++ b/app/partials/common/components/taskboard-placeholder.jade @@ -0,0 +1,7 @@ +.placeholder-avatar + .image + .text + .line + .line +p.title {{'TASKBOARD.PLACEHOLDER_CARD_TITLE' | translate}} +p {{'TASKBOARD.PLACEHOLDER_CARD_TEXT' | translate}} diff --git a/app/partials/common/components/watchers.jade b/app/partials/common/components/watchers.jade index b11a4aeb..31e4ea94 100644 --- a/app/partials/common/components/watchers.jade +++ b/app/partials/common/components/watchers.jade @@ -1,22 +1,29 @@ -<% if(isEditable){ %> -.watchers-header - span.title(translate="COMMON.WATCHERS.TITLE") - a.icon.icon-plus.add-watcher(href="", title="{{'COMMON.WATCHERS.ADD' | translate}}") -<% } else if(watchers.length > 0){ %> -.watchers-header - span.title(translate="COMMON.WATCHERS.TITLE") -<% }; %> - <% _.each(watchers, function(watcher) { %> <% if(watcher) { %> -.watcher-single - .watcher-avatar - img(src!="<%- watcher.photo %>" alt!="<%- watcher.full_name_display %>") - .watcher-name +.user-list-single + .user-list-avatar + img( + src!="<%- watcher.photo %>" + alt!="<%- watcher.full_name_display %>" + ) + .user-list-name span <%- watcher.full_name_display %> <% if(isEditable){ %> - a.icon.icon-delete(data-watcher-id!="<%- watcher.id %>" href="" title="{{'COMMON.WATCHERS.DELETE' | translate}}") + a.icon.icon-delete.js-delete-watcher( + href="" + data-watcher-id!="<%- watcher.id %>" + title="{{'COMMON.WATCHERS.DELETE' | translate}}" + ) <% }; %> <% } %> <% }); %> + +<% if(isEditable){ %> +a.add-watcher.js-add-watcher( + href="" + title="{{'COMMON.WATCHERS.TITLE_ADD' | translate}}" +) + span.icon.icon-plus + span(translate="COMMON.WATCHERS.ADD") +<% }; %> diff --git a/app/partials/common/estimation/us-estimation-points-per-role.jade b/app/partials/common/estimation/us-estimation-points-per-role.jade index 8fd9ce85..904d6b4a 100644 --- a/app/partials/common/estimation/us-estimation-points-per-role.jade +++ b/app/partials/common/estimation/us-estimation-points-per-role.jade @@ -1,9 +1,9 @@ ul.points-per-role - li.total + li.ticket-role-points.total span.points <%- totalPoints %> span.role(translate="US.TOTAL_POINTS") <% _.each(roles, function(role) { %> - li(class!="total <% if(editable){ %>clickable<% } %>", data-role-id!="<%- role.id %>", title!="<%- role.name %>") + li.ticket-role-points(class!="total <% if(editable){ %>clickable<% } %>", data-role-id!="<%- role.id %>", title!="<%- role.name %>") span.points <%- role.points %> span.role <%- role.name %> <% }); %> diff --git a/app/partials/common/history/history-activity.jade b/app/partials/common/history/history-activity.jade index d9c225ca..ee6152fa 100644 --- a/app/partials/common/history/history-activity.jade +++ b/app/partials/common/history/history-activity.jade @@ -1,10 +1,10 @@ div(class!="activity-single <%- mode %>") .activity-user - a.avatar(href="" title!="<%- userFullName %>") + a.avatar(href!="<%- userProfileUrl %>", title!="<%- userFullName %>") img(src!="<%- avatar %>", alt!="<%- userFullName %>") .activity-content .activity-username - a.username(href="", title!="<%- userFullName %>") + a.username(href!="<%- userProfileUrl %>", title!="<%- userFullName %>") | <%- userFullName %> span.date | <%- creationDate %> diff --git a/app/partials/common/history/history-base.jade b/app/partials/common/history/history-base.jade index d77e1065..4ca4a9af 100644 --- a/app/partials/common/history/history-base.jade +++ b/app/partials/common/history/history-base.jade @@ -1,11 +1,11 @@ section.history ul.history-tabs li - a(href="#", class="active") + a(href="#", class="active", data-section-class="history-comments") span.icon.icon-comment span.tab-title(translate="COMMENTS.TITLE") li - a(href="#") + a(href="#", data-section-class="history-activity") span.icon.icon-issues span.tab-title(translate="ACTIVITY.TITLE") section.history-comments diff --git a/app/partials/common/lightbox/lightbox-assigned-to-users.jade b/app/partials/common/lightbox/lightbox-assigned-to-users.jade index 792d4ec3..49c44b9a 100644 --- a/app/partials/common/lightbox/lightbox-assigned-to-users.jade +++ b/app/partials/common/lightbox/lightbox-assigned-to-users.jade @@ -1,23 +1,38 @@ <% if (selected) { %> -.watcher-single.active - .watcher-avatar - a(href="", title="{{'COMMON.ASSIGNED_TO' | translate}}", class="avatar") +.user-list-single.is-active + .user-list-avatar + a( + href="" + title="{{'COMMON.ASSIGNED_TO' | translate}}" + ) img(src!="<%- selected.photo %>") - a(href="", title!="<%- selected.full_name_display %>", class="watcher-name") + a.user-list-name( + href="" + title!="<%- selected.full_name_display %>" + ) | <%-selected.full_name_display %> - a(href="", title="{{'COMMON.ASSIGNED_TO.REMOVE_ASSIGNED' | translate}}", class="icon icon-delete remove-assigned-to") + a.icon-delete.remove-assigned-to( + href="" + title="{{'COMMON.ASSIGNED_TO.REMOVE_ASSIGNED' | translate}}" + ) <% } %> <% _.each(users, function(user) { %> -.watcher-single(data-user-id!="<%- user.id %>") - .watcher-avatar - a(href="#", title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}", class="avatar") +.user-list-single(data-user-id!="<%- user.id %>") + .user-list-avatar + a( + href="#" + title="{{'COMMON.ASSIGNED_TO.TITLE' | translate}}" + ) img(src!="<%- user.photo %>") - a(href="", title!="<%- user.full_name_display %>", class="watcher-name") + a.user-list-name( + href="" + title!="<%- user.full_name_display %>" + ) | <%- user.full_name_display %> <% }) %> <% if (showMore) { %> -div(ng-show="filteringUsers", class="more-watchers") +.more-watchers span(translate="COMMON.ASSIGNED_TO.TOO_MANY") <% } %> diff --git a/app/partials/common/lightbox/lightbox-assigned-to.jade b/app/partials/common/lightbox/lightbox-assigned-to.jade index 34e87b9a..7f00751d 100644 --- a/app/partials/common/lightbox/lightbox-assigned-to.jade +++ b/app/partials/common/lightbox/lightbox-assigned-to.jade @@ -6,4 +6,4 @@ div.form input(type="text", data-maxlength="500", placeholder="{{'LIGHTBOX.ASSIGNED_TO.SEARCH' | translate}}", ng-model="usersSearch") //-This block is rendered by the directive - div.watchers + .assigned-to-list diff --git a/app/partials/common/lightbox/lightbox-attachment-preview.jade b/app/partials/common/lightbox/lightbox-attachment-preview.jade new file mode 100644 index 00000000..91932ed7 --- /dev/null +++ b/app/partials/common/lightbox/lightbox-attachment-preview.jade @@ -0,0 +1,6 @@ +.attachment-preview + a.close(href="", title="{{'COMMON.CLOSE' | translate}}") + span.icon.icon-delete + + a(href!="<%- url %>", title!="<%- title %>", target="_blank", download!="<%- name %>") + img(src!="<%- url %>") \ No newline at end of file diff --git a/app/partials/common/lightbox/lightbox-users.jade b/app/partials/common/lightbox/lightbox-users.jade index 180b8fb3..2c43e246 100644 --- a/app/partials/common/lightbox/lightbox-users.jade +++ b/app/partials/common/lightbox/lightbox-users.jade @@ -4,5 +4,5 @@ div.form h2.title(translate="COMMON.WATCHERS.ADD") fieldset input(type="text", data-maxlength="500", placeholder="{{'LIGHTBOX.ASSIGNED_TO.SEARCH' | translate}}", ng-model="usersSearch") - div.watchers + div.ticket-watchers //- The content of this is rendered by directive diff --git a/app/partials/custom-attributes/custom-attribute-value-edit.jade b/app/partials/custom-attributes/custom-attribute-value-edit.jade index 900518bb..f6869cba 100644 --- a/app/partials/custom-attributes/custom-attribute-value-edit.jade +++ b/app/partials/custom-attributes/custom-attribute-value-edit.jade @@ -1,6 +1,6 @@ form.custom-field-single.editable div.custom-field-data - label.custom-field-name(for="custom-field-description") + label.custom-field-name(for="custom-field-value") <%- name %> <% if (description){ %> span.custom-field-description @@ -8,7 +8,16 @@ form.custom-field-single.editable <% } %> div.custom-field-value - input#custom-field-description(name="description", type="text", value!="<%- value %>") + //- See TYPE_CHOICES in app/coffee/modules/common/custom-field-values.coffee + <% if (type=="text") { %> + input#custom-field-value(name="value", type="text", value!="<%- value %>") + <% } else if (type=="multiline") { %> + textarea#custom-field-value(name="value") <%- value %> + <% } else if (type=="date") { %> + input#custom-field-value(name="value", type="text", value!="<%- value %>") + <% } else { %> + input#custom-field-value(name="value", type="text", value!="<%- value %>") + <% } %> div.custom-field-options a.icon.icon-floppy(href="", title="{{'COMMON.CUSTOM_ATTRIBUTES.SAVE' | translate}}") diff --git a/app/partials/custom-attributes/custom-attribute-value.jade b/app/partials/custom-attributes/custom-attribute-value.jade index 499bce57..f7ac5620 100644 --- a/app/partials/custom-attributes/custom-attribute-value.jade +++ b/app/partials/custom-attributes/custom-attribute-value.jade @@ -7,7 +7,7 @@ div.custom-field-single <%- description %> <% } %> - div.custom-field-value.read-mode + div.custom-field-value.js-value-view-mode span <%- value %> diff --git a/app/partials/error/error.jade b/app/partials/error/error.jade index 356339f8..39629937 100644 --- a/app/partials/error/error.jade +++ b/app/partials/error/error.jade @@ -6,5 +6,4 @@ div.error-main img(src="/images/logo.png", alt="TAIGA") h1.logo Taiga p.error-text(translate="ERROR.TEXT1") - p.error-text(translate="ERROR.TEXT2") a(href="/", title="", translate="COMMON.GO_HOME") diff --git a/app/partials/error/permission-denied.jade b/app/partials/error/permission-denied.jade index 6f3b8efb..12869f63 100644 --- a/app/partials/error/permission-denied.jade +++ b/app/partials/error/permission-denied.jade @@ -5,5 +5,5 @@ div.error-main object.logo-svg(type="image/svg+xml", data="/svg/logo.svg") img(src="/images/logo.png", alt="TAIGA") h1.logo(translate="ERROR.PERMISSION_DENIED") - p.error-text(translate="ERROR.PERMISSION_DENIED_CODE") + p.error-text(translate="ERROR.PERMISSION_DENIED_TEXT") a(href="/", title="", translate="COMMON.GO_HOME") diff --git a/app/partials/includes/components/backlog-row.jade b/app/partials/includes/components/backlog-row.jade index 2de874a9..99684c1b 100644 --- a/app/partials/includes/components/backlog-row.jade +++ b/app/partials/includes/components/backlog-row.jade @@ -1,18 +1,44 @@ -div.row.us-item-row(ng-repeat="us in visibleUserstories track by us.id", tg-bind-scope, ng-class="{blocked: us.is_blocked}", tg-class-permission="{'readonly': '!modify_us'}") +div.row.us-item-row( + ng-repeat="us in userstories track by us.id" + tg-bind-scope + ng-class="{blocked: us.is_blocked}" + tg-class-permission="{'readonly': '!modify_us'}" +) + div.input(tg-check-permission="modify_us") + input( + type="checkbox" + name="" + ) + div.votes( + ng-class="{'inactive': !us.total_voters, 'is-voted': us.is_voter}" + title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:us.total_voters||0}:'messageformat' }}" + ) + span.icon.icon-caret-up + span {{ ::us.total_voters }} div.user-stories div.tags-block(tg-colorize-tags="us.tags", tg-colorize-tags-type="backlog") div.user-story-name - input(tg-check-permission="modify_us", type="checkbox", name="") - a.clickable(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", - tg-nav-get-params="{\"no-milestone\": 1}", - title="#{{ us.ref }} {{ us.subject }}") + a.clickable( + href="" + tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" + tg-nav-get-params="{\"no-milestone\": 1}" + title="#{{ us.ref }} {{ us.subject }}" + ) span(tg-bo-ref="us.ref") span(ng-bind="us.subject") div.us-settings - a.icon.icon-edit(tg-check-permission="modify_us", href="", - ng-click="ctrl.editUserStory(us.project, us.ref, $event)", title="{{'COMMON.EDIT' | translate}}") - a.icon.icon-delete(tg-check-permission="delete_us", href="", - ng-click="ctrl.deleteUserStory(us)", title="{{'COMMON.DELETE' | translate}}") + a.icon.icon-edit( + href="" + tg-check-permission="modify_us" + ng-click="ctrl.editUserStory(us.project, us.ref, $event)" + title="{{'COMMON.EDIT' | translate}}" + ) + a.icon.icon-delete( + href="" + tg-check-permission="delete_us" + ng-click="ctrl.deleteUserStory(us)" + title="{{'COMMON.DELETE' | translate}}" + ) div.status(tg-us-status="us" on-update="ctrl.updateUserStoryStatus()") a.us-status(href="", title="{{'BACKLOG.STATUS_NAME' | translate}}") diff --git a/app/partials/includes/components/empty-search-results.jade b/app/partials/includes/components/empty-search-results.jade new file mode 100644 index 00000000..be261c6c --- /dev/null +++ b/app/partials/includes/components/empty-search-results.jade @@ -0,0 +1,6 @@ +img( + src="../../images/search-empty.png" + alt="{{ 'SEARCH.EMPTY_TITLE' | translate }}" +) +p.title {{ 'SEARCH.EMPTY_TITLE' | translate }} +p {{ 'SEARCH.EMPTY_DESCRIPTION' | translate }} diff --git a/app/partials/includes/components/notification-message.jade b/app/partials/includes/components/notification-message.jade index d0feaeaf..82b3ab05 100644 --- a/app/partials/includes/components/notification-message.jade +++ b/app/partials/includes/components/notification-message.jade @@ -25,4 +25,5 @@ div.notification-light.notification-message-light-error div.text h4.warning(translate="NOTIFICATION.WARNING") p(translate="NOTIFICATION.WARNING_TEXT") - a.icon.icon-delete(href="", title="{{'NOTIFICATION.CLOSE' | translate}}") + a.close(href="", title="{{'NOTIFICATION.CLOSE' | translate}}") + include ../../../svg/remove.svg diff --git a/app/partials/includes/components/sprint-summary.jade b/app/partials/includes/components/sprint-summary.jade index caa16ba2..5fbe8e4e 100644 --- a/app/partials/includes/components/sprint-summary.jade +++ b/app/partials/includes/components/sprint-summary.jade @@ -24,5 +24,6 @@ div.summary.large-summary span.icon.icon-iocaine span.number(ng-bind="stats.iocaine_doses|default:'--'") span.description(translate="BACKLOG.SPRINT_SUMMARY.IOCAINE_DOSES") - - a.icon.icon-stats.toggle-analytics-visibility(href="", title="{{'BACKLOG.SPRINT_SUMMARY.SHOW_STATISTICS_TITLE' | translate}}") + + div.stats.toggle-analytics-visibility(title="{{'BACKLOG.SPRINT_SUMMARY.SHOW_STATISTICS_TITLE' | translate}}") + include ../../../svg/graph.svg diff --git a/app/partials/includes/components/summary.jade b/app/partials/includes/components/summary.jade index 1b29810d..e81b0b05 100644 --- a/app/partials/includes/components/summary.jade +++ b/app/partials/includes/components/summary.jade @@ -4,7 +4,7 @@ div.summary div.data span.number(ng-bind="stats.completedPercentage + '%'") - div.summary-stats + div.summary-stats(ng-if="stats.total_points") span.number(ng-bind="stats.total_points") -- span.description(translate="BACKLOG.SUMMARY.PROJECT_POINTS") div.summary-stats @@ -16,3 +16,10 @@ div.summary div.summary-stats span.number(ng-bind="stats.speed | number:0") -- span.description(translate="BACKLOG.SUMMARY.POINTS_PER_SPRINT") + + + div.stats.js-toggle-burndown-visibility-button( + title="{{'BACKLOG.SPRINT_SUMMARY.TOGGLE_BAKLOG_GRAPH' | translate}}", + ng-if="!showGraphPlaceholder" + ) + include ../../../svg/graph.svg diff --git a/app/partials/includes/modules/admin/admin-custom-attributes.jade b/app/partials/includes/modules/admin/admin-custom-attributes.jade index cab78fc8..8d2ef76f 100644 --- a/app/partials/includes/modules/admin/admin-custom-attributes.jade +++ b/app/partials/includes/modules/admin/admin-custom-attributes.jade @@ -1,7 +1,10 @@ section.custom-fields-table.basic-table div.project-values-title h2 {{ customFieldSectionTitle | translate }} - a.button.button-gray.show-add-new.js-add-custom-field-button(href="", title="{{ customFieldButtonTitle | translate }}") + a.button.button-gray.show-add-new.js-add-custom-field-button( + href="" + title="{{ customFieldButtonTitle | translate }}" + ) span(translate="ADMIN.CUSTOM_ATTRIBUTES.ADD") div.table-header @@ -10,6 +13,8 @@ section.custom-fields-table.basic-table span(translate="COMMON.FIELDS.NAME") div.custom-description span(translate="COMMON.FIELDS.DESCRIPTION") + div.custom-field-type + span(translate="COMMON.FIELDS.TYPE") div.custom-options div.table-body @@ -22,33 +27,84 @@ section.custom-fields-table.basic-table span {{ attr.name }} div.custom-description span {{ attr.description }} + div.custom-field-type(ng-switch on="attr.type") + //- See TYPE_CHOICES in app/coffee/modules/admin/project-values.coffee + span(ng-switch-default, translate="ADMIN.CUSTOM_FIELDS.FIELD_TYPE_TEXT") + span(ng-switch-when="multiline", translate="ADMIN.CUSTOM_FIELDS.FIELD_TYPE_MULTI") + span(ng-switch-when="date", translate="ADMIN.CUSTOM_FIELDS.FIELD_TYPE_DATE") div.custom-options div.custom-options-wrapper - a.js-edit-custom-field-button.icon.icon-edit(href="", title="{{'ADMIN.CUSTOM_ATTRIBUTES.EDIT' | translate}}") - a.js-delete-custom-field-button.icon.icon-delete(href="", title="{{'ADMIN.CUSTOM_ATTRIBUTES.DELETE' | translate}}") + a.js-edit-custom-field-button.icon.icon-edit( + href="" + title="{{'ADMIN.CUSTOM_ATTRIBUTES.EDIT' | translate}}" + ) + a.js-delete-custom-field-button.icon.icon-delete( + href="" + title="{{'ADMIN.CUSTOM_ATTRIBUTES.DELETE' | translate}}" + ) div.row.single-custom-field.js-edit-custom-field.hidden fieldset.custom-name - input(type="text", name="name", placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_NAME' | translate}}", - ng-model="attr.name", data-required="true" data-maxlength="64") + input( + type="text" + name="name" + placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_NAME' | translate}}" + ng-model="attr.name" + data-required="true" + data-maxlength="64" + ) fieldset.custom-description - input(type="text", name="description", placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_DESCRIPTION' | translate}}", - ng-model="attr.description") - + input( + type="text" + name="description" + placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_DESCRIPTION' | translate}}" + ng-model="attr.description" + ) + fieldset.custom-field-type + select( + ng-model="attr.type" + ng-options="type.key as type.name | translate for type in TYPE_CHOICES" + ) fieldset.custom-options div.custom-options-wrapper - a.js-update-custom-field-button.icon.icon-floppy(href="", title="{{'ADMIN.CUSTOM_ATTRIBUTES.ACTION_UPDATE' | translate}}") - a.js-cancel-edit-custom-field-button.icon.icon-delete(href="", title="{{'ADMIN.CUSTOM_ATTRIBUTES.ACTION_CANCEL_EDITION' | translate}}") + a.js-update-custom-field-button.icon.icon-floppy( + href="" + title="{{'ADMIN.CUSTOM_ATTRIBUTES.ACTION_UPDATE' | translate}}" + ) + a.js-cancel-edit-custom-field-button.icon.icon-delete( + href="" + title="{{'ADMIN.CUSTOM_ATTRIBUTES.ACTION_CANCEL_EDITION' | translate}}" + ) form.row.single-custom-field.js-new-custom-field.hidden fieldset.custom-name - input(type="text", name="name", placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_NAME' | translate}}", - ng-model="newAttr.name", data-required="true", data-maxlength="64") + input( + type="text" + name="name" + placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_NAME' | translate}}" + ng-model="newAttr.name" + data-required="true" + data-maxlength="64" + ) fieldset.custom-description - input(type="text", name="description", placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_DESCRIPTION' | translate}}", - ng-model="newAttr.description") - + input( + type="text" + name="description" + placeholder="{{'ADMIN.CUSTOM_ATTRIBUTES.SET_FIELD_DESCRIPTION' | translate}}" + ng-model="newAttr.description" + ) + fieldset.custom-field-type + select( + ng-model="newAttr.type" + ng-options="type.key as type.name for type in TYPE_CHOICES" + ) fieldset.custom-options div.custom-options-wrapper - a.js-create-custom-field-button.icon.icon-floppy(href="", title="{{'ADMIN.CUSTOM_ATTRIBUTES.SAVE_TITLE' | translate}}") - a.js-cancel-new-custom-field-button.icon.icon-delete(href="", title="{{'ADMIN.CUSTOM_ATTRIBUTES.CANCEL_TITLE' | translate}}") + a.js-create-custom-field-button.icon.icon-floppy( + href="" + title="{{'ADMIN.CUSTOM_ATTRIBUTES.SAVE_TITLE' | translate}}" + ) + a.js-cancel-new-custom-field-button.icon.icon-delete( + href="" + title="{{'ADMIN.CUSTOM_ATTRIBUTES.CANCEL_TITLE' | translate}}" + ) diff --git a/app/partials/includes/modules/backlog-filters.jade b/app/partials/includes/modules/backlog-filters.jade index 5533851d..056f65a0 100644 --- a/app/partials/includes/modules/backlog-filters.jade +++ b/app/partials/includes/modules/backlog-filters.jade @@ -18,7 +18,7 @@ section.filters div.filters-cats ul li - a(href="", title="{{'BACKLOG.FILTERS.FILTER_CATEGORY_STATUS' | translate}}", data-type="statuses") + a(href="", title="{{'BACKLOG.FILTERS.FILTER_CATEGORY_STATUS' | translate}}", data-type="status") span.title(translate="BACKLOG.FILTERS.FILTER_CATEGORY_STATUS") span.icon.icon-arrow-right li diff --git a/app/partials/includes/modules/backlog-table.jade b/app/partials/includes/modules/backlog-table.jade index ce2c5439..9c3eab4c 100644 --- a/app/partials/includes/modules/backlog-table.jade +++ b/app/partials/includes/modules/backlog-table.jade @@ -1,5 +1,7 @@ div.backlog-table-header div.row.backlog-table-title + div.input(tg-check-permission="modify_us") + div.votes(translate="COMMON.FIELDS.VOTES") div.user-stories(translate="BACKLOG.TABLE.COLUMN_US") div.status(translate="COMMON.FIELDS.STATUS") div.points(tg-us-role-points-selector, title="{{'BACKLOG.TABLE.TITLE_COLUMN_POINTS' | translate}}") diff --git a/app/partials/includes/modules/change-email-form.jade b/app/partials/includes/modules/change-email-form.jade index 74d15ad6..66f38189 100644 --- a/app/partials/includes/modules/change-email-form.jade +++ b/app/partials/includes/modules/change-email-form.jade @@ -6,8 +6,17 @@ div.change-email-form(tg-change-email) form fieldset - input(type="hidden", name="email_token", ng-model="data.email_token", data-required="true", - placeholder="{{'CHANGE_EMAIL_FORM.PLACEHOLDER_INPUT_TOKEN' | translate}}") + input( + type="hidden" + name="email_token" + ng-model="data.email_token" + data-required="true" + placeholder="{{'CHANGE_EMAIL_FORM.PLACEHOLDER_INPUT_TOKEN' | translate}}" + ) - a.button-change-email.button-gray(href="", title="{{'CHANGE_EMAIL_FORM.ACTION_CHANGE_EMAIL' | translate}}", translate="CHANGE_EMAIL_FORM.ACTION_CHANGE_EMAIL") + a.button-change-email.button-gray( + href="" + title="{{'CHANGE_EMAIL_FORM.ACTION_CHANGE_EMAIL' | translate}}" + translate="CHANGE_EMAIL_FORM.ACTION_CHANGE_EMAIL" + ) button(type="submit", class="hidden") diff --git a/app/partials/includes/modules/change-password-from-recovery-form.jade b/app/partials/includes/modules/change-password-from-recovery-form.jade index 3c64b179..34a0a942 100644 --- a/app/partials/includes/modules/change-password-from-recovery-form.jade +++ b/app/partials/includes/modules/change-password-from-recovery-form.jade @@ -5,16 +5,30 @@ div.change-password-form-container(tg-change-password-from-recovery) span(translate="CHANGE_PASSWORD_RECOVERY_FORM.SUBTITLE") form - fieldset.token-change-password(ng-hide="tokenInParams") - input(type="text", name="token", ng-model="data.token", data-required="true", - placeholder="{{'CHANGE_PASSWORD_RECOVERY_FORM.PLACEHOLDER_RECOVER_PASSWORD_TOKEN' | translate}}") - a.get-token(href="", tg-nav="forgot-password", - title="{{'CHANGE_PASSWORD_RECOVERY_FORM.TITLE_LINK_NEED_TOKEN' | translate}}", translate="CHANGE_PASSWORD_RECOVERY_FORM.LINK_NEED_TOKEN") fieldset - input(type="password", name="password", id="password", ng-model="data.password", - data-required="true", placeholder="{{'CHANGE_PASSWORD_RECOVERY_FORM.PLACEHOLDER_NEW_PASSWORD' | translate}}") + input( + type="password" + name="password" + id="password" + tg-capslock + ng-model="data.password" + data-required="true" + placeholder="{{'CHANGE_PASSWORD_RECOVERY_FORM.PLACEHOLDER_NEW_PASSWORD' | translate}}" + ) fieldset - input(type="password", name="password2", id="password2", ng-model="data.password2", - data-required="true", data-equalto="#password", placeholder="{{'CHANGE_PASSWORD_RECOVERY_FORM.PLACEHOLDER_RE_TYPE_NEW_PASSWORD' | translate}}") + input( + type="password" + name="password2" + id="password2" + tg-capslock + ng-model="data.password2" + data-required="true" + data-equalto="#password" + placeholder="{{'CHANGE_PASSWORD_RECOVERY_FORM.PLACEHOLDER_RE_TYPE_NEW_PASSWORD' | translate}}" + ) fieldset - button.button-change-password.button-gray.submit-button(type="submit", title="{{'CHANGE_PASSWORD_RECOVERY_FORM.ACTION_RESET_PASSWORD' | translate}}", translate="CHANGE_PASSWORD_RECOVERY_FORM.ACTION_RESET_PASSWORD") + button.button-change-password.button-gray.submit-button( + type="submit" + title="{{'CHANGE_PASSWORD_RECOVERY_FORM.ACTION_RESET_PASSWORD' | translate}}" + translate="CHANGE_PASSWORD_RECOVERY_FORM.ACTION_RESET_PASSWORD" + ) diff --git a/app/partials/includes/modules/forgot-form.jade b/app/partials/includes/modules/forgot-form.jade index ce276287..6bd1e5cd 100644 --- a/app/partials/includes/modules/forgot-form.jade +++ b/app/partials/includes/modules/forgot-form.jade @@ -6,9 +6,18 @@ div.forgot-form-container(tg-forgot-password) form(ng-submit="ctrl.submit()") fieldset - input(type="text", name="username", ng-model="data.username", data-required="true", - placeholder="{{'FORGOT_PASSWORD_FORM.PLACEHOLDER_FIELD' | translate}}") + input( + type="text" + name="username" + ng-model="data.username" + data-required="true" + placeholder="{{'FORGOT_PASSWORD_FORM.PLACEHOLDER_FIELD' | translate}}" + ) fieldset - button.button-gray.submit-button.button-forgot(type="submit", title="{{'FORGOT_PASSWORD_FORM.ACTION_RESET_PASSWORD' | translate}}", translate="FORGOT_PASSWORD_FORM.ACTION_RESET_PASSWORD") + button.button-gray.submit-button.button-forgot( + type="submit" + title="{{'FORGOT_PASSWORD_FORM.ACTION_RESET_PASSWORD' | translate}}" + translate="FORGOT_PASSWORD_FORM.ACTION_RESET_PASSWORD" + ) a(href="", tg-nav="login", translate="FORGOT_PASSWORD_FORM.LINK_CANCEL") diff --git a/app/partials/includes/modules/invitation-login-form.jade b/app/partials/includes/modules/invitation-login-form.jade index b089d1c0..2d5e2a3f 100644 --- a/app/partials/includes/modules/invitation-login-form.jade +++ b/app/partials/includes/modules/invitation-login-form.jade @@ -2,19 +2,36 @@ form.login-form p.form-header(translate="LOGIN_COMMON.HEADER") fieldset - input(type="text", name="username", ng-model="dataLogin.username", data-required="true", - placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_NAME' | translate}}") + input( + type="text" + name="username" + ng-model="dataLogin.username" + data-required="true" + placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_NAME' | translate}}" + ) fieldset.login-password - input(type="password", name="password", ng-model="dataLogin.password", data-required="true", - placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_PASSWORD' | translate}}") - a.forgot-pass(href="", tg-nav="forgot-password", - title="{{'LOGIN_COMMON.TITLE_LINK_FORGOT_PASSWORD' | translate}}", - translate="LOGIN_COMMON.LINK_FORGOT_PASSWORD") + input( + type="password" + name="password" + tg-capslock + ng-model="dataLogin.password" + data-required="true" + placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_PASSWORD' | translate}}" + ) + // This should be hidden when focus on pass + a.forgot-pass( + href="" + tg-nav="forgot-password" + title="{{'LOGIN_COMMON.TITLE_LINK_FORGOT_PASSWORD' | translate}}" + translate="LOGIN_COMMON.LINK_FORGOT_PASSWORD" + ) fieldset - a.button-login.button-blackish.submit-button(type="submit", - title="{{'LOGIN_COMMON.ACTION_ENTER' | translate}}", - translate="LOGIN_COMMON.ACTION_ENTER") + a.button-login.button-blackish.submit-button( + type="submit" + title="{{'LOGIN_COMMON.ACTION_ENTER' | translate}}" + translate="LOGIN_COMMON.ACTION_ENTER" + ) fieldset(ng-repeat="plugin in contribPlugins|filter:{type: 'auth'}", ng-include="plugin.template") diff --git a/app/partials/includes/modules/invitation-register-form.jade b/app/partials/includes/modules/invitation-register-form.jade index 4eeace6a..911e62e0 100644 --- a/app/partials/includes/modules/invitation-register-form.jade +++ b/app/partials/includes/modules/invitation-register-form.jade @@ -1,25 +1,52 @@ form.register-form p.form-header(translate="REGISTER_FORM.TITLE") fieldset - input(type="text", name="username", ng-model="dataRegister.username", - data-required="true", data-maxlength="255", data-regexp="^[\\w.-]+$", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_NAME' | translate}}") + input( + type="text" + name="username" + ng-model="dataRegister.username" + data-required="true" + data-maxlength="255" + data-regexp="^[\\w.-]+$" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_NAME' | translate}}" + ) fieldset - input(type="text", name="full_name", ng-model="dataRegister.full_name", - data-required="true", data-maxlength="256", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_FULL_NAME' | translate}}") + input( + type="text" + name="full_name" + ng-model="dataRegister.full_name" + data-required="true" + data-maxlength="256" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_FULL_NAME' | translate}}" + ) fieldset - input(type="text", name="email", ng-model="dataRegister.email", - data-required="true", data-maxlength="255", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_EMAIL' | translate}}") + input( + type="text" + name="email" + ng-model="dataRegister.email" + data-required="true" + data-maxlength="255" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_EMAIL' | translate}}" + ) fieldset - input(type="password", name="password", ng-model="dataRegister.password", data-required="true", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_PASSWORD' | translate}}") + input( + type="password" + name="password" + tg-capslock + ng-model="dataRegister.password" + data-required="true" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_PASSWORD' | translate}}" + ) fieldset - a.button-register.button-blackish.submit-button(type="submit", title="{{'REGISTER_FORM.ACTION_SIGN_UP' | translate}}", translate="REGISTER_FORM.ACTION_SIGN_UP") + a.button-register.button-blackish.submit-button( + type="submit" + title="{{'REGISTER_FORM.ACTION_SIGN_UP' | translate}}" + translate="REGISTER_FORM.ACTION_SIGN_UP" + ) - tg-terms-notice + //- Only displays terms notice when terms plugin is loaded. + tg-terms-of-service-and-privacy-policy-notice diff --git a/app/partials/includes/modules/issues-filters.jade b/app/partials/includes/modules/issues-filters.jade index 3b427086..ab735773 100644 --- a/app/partials/includes/modules/issues-filters.jade +++ b/app/partials/includes/modules/issues-filters.jade @@ -22,7 +22,7 @@ section.filters span.title(translate="ISSUES.FILTERS.CATEGORIES.TYPE") span.icon.icon-arrow-right li - a(href="", title="{{ 'ISSUES.FILTERS.CATEGORIES.STATUS' | translate}}", data-type="statuses") + a(href="", title="{{ 'ISSUES.FILTERS.CATEGORIES.STATUS' | translate}}", data-type="status") span.title(translate="ISSUES.FILTERS.CATEGORIES.STATUS") span.icon.icon-arrow-right li diff --git a/app/partials/includes/modules/list-filters.jade b/app/partials/includes/modules/issues-options.jade similarity index 80% rename from app/partials/includes/modules/list-filters.jade rename to app/partials/includes/modules/issues-options.jade index 5ebb73a8..4cbce556 100644 --- a/app/partials/includes/modules/list-filters.jade +++ b/app/partials/includes/modules/issues-options.jade @@ -1,4 +1,4 @@ -section.list-filters(tg-check-permission="add_issue") +section.issues-options(tg-check-permission="add_issue") div.new-issue a.button-green(href="", ng-click="ctrl.addNewIssue()") span.text(translate="ISSUES.ACTION_NEW_ISSUE") diff --git a/app/partials/includes/modules/issues-table.jade b/app/partials/includes/modules/issues-table.jade index d3672c83..e33b7d01 100644 --- a/app/partials/includes/modules/issues-table.jade +++ b/app/partials/includes/modules/issues-table.jade @@ -3,23 +3,44 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}") div.level-field(data-fieldname="type", translate="ISSUES.TABLE.COLUMNS.TYPE") div.level-field(data-fieldname="severity", translate="ISSUES.TABLE.COLUMNS.SEVERITY") div.level-field(data-fieldname="priority", translate="ISSUES.TABLE.COLUMNS.PRIORITY") + div.votes(data-fieldname="total_voters", translate="ISSUES.TABLE.COLUMNS.VOTES") div.subject(data-fieldname="subject", translate="ISSUES.TABLE.COLUMNS.SUBJECT") div.issue-field(data-fieldname="status", translate="ISSUES.TABLE.COLUMNS.STATUS") div.created-field(data-fieldname="created_date", translate="ISSUES.TABLE.COLUMNS.CREATED") div.assigned-field(data-fieldname="assigned_to", translate="ISSUES.TABLE.COLUMNS.ASSIGNED_TO") - div.row.table-main(ng-repeat="issue in issues track by issue.id") + div.row.table-main( + ng-repeat="issue in issues track by issue.id" + ng-class="{'is-blocked': issue.is_blocked}" + ) div.level-field(tg-listitem-type="issue") div.level-field(tg-listitem-severity="issue") div.level-field(tg-listitem-priority="issue") + div.votes( + ng-class="{'inactive': !issue.total_voters, 'is-voted': issue.is_voter}" + title="{{ 'COMMON.VOTE_BUTTON.COUNTER_TITLE'|translate:{total:issue.total_voters||0}:'messageformat' }}" + ) + span.icon.icon-caret-up + span {{ ::issue.total_voters }} div.subject - a(href="", tg-nav="project-issues-detail:project=project.slug,ref=issue.ref", - title="#{{ ::issue.ref }} {{ ::issue.subject }}") + a( + href="" + tg-nav="project-issues-detail:project=project.slug,ref=issue.ref" + title="#{{ ::issue.ref }} {{ ::issue.subject }}" + ) span(tg-bo-ref="issue.ref") + span.blocked-text( + ng-if="issue.is_blocked" + title="{{issue.blocked_note}}" + ) {{'ISSUES.TABLE.BLOCKED' | translate}} span(ng-bind="issue.subject") + div.issue-field(tg-issue-status-inline-edition="issue") - a.issue-status(href="", title="{{'ISSUES.TABLE.TITLE_ACTION_CHANGE_STATUS' | translate}}") + a.issue-status( + href="" + title="{{'ISSUES.TABLE.TITLE_ACTION_CHANGE_STATUS' | translate}}" + ) span.issue-status-bind span.icon.icon-arrow-bottom(tg-check-permission="modify_issue") @@ -30,10 +51,10 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}") figure.avatar span.icon.icon-arrow-bottom(tg-check-permission="modify_issue") -section.empty.empty-issues(ng-class="{hidden: issues.length}") - span.icon.icon-issues - span.title(translate="ISSUES.TABLE.EMPTY.TITLE") - span(translate="ISSUES.TABLE.EMPTY.SUBTITLE") - a(href="", ng-click="ctrl.addNewIssue()", - title="{{'ISSUES.TABLE.EMPTY.ACTION_CREATE_ISSUE' | translate}}", - translate="ISSUES.TABLE.EMPTY.ACTION_CREATE_ISSUE") +section.empty-issues(ng-if="!issues.length") + img( + src="../../images/issues-empty.png", + alt="{{ISSUES.TABLE.EMPTY.TITLE | translate }}" + ) + p.title(translate="ISSUES.TABLE.EMPTY.TITLE") + p(translate="ISSUES.TABLE.EMPTY.SUBTITLE") diff --git a/app/partials/includes/modules/kanban-table.jade b/app/partials/includes/modules/kanban-table.jade index 7082a0ce..5770b403 100644 --- a/app/partials/includes/modules/kanban-table.jade +++ b/app/partials/includes/modules/kanban-table.jade @@ -31,24 +31,30 @@ div.kanban-table(tg-kanban-squish-column) tg-check-permission="add_us", ng-hide="s.is_archived") - a(href="", - ng-attr-title="{{title}}", - ng-class="class" - ng-if="s.is_archived", - tg-kanban-archived-status-header="s") + a( + href="" + ng-attr-title="{{title}}" + ng-class="class" + ng-if="s.is_archived" + tg-kanban-archived-status-header="s") + include ../../../svg/eye.svg div.kanban-table-body div.kanban-table-inner div.kanban-uses-box.task-column(ng-class='{vfold:folds[s.id]}', - ng-repeat="s in usStatusList track by s.id", - tg-kanban-sortable, - tg-kanban-wip-limit="s", - tg-kanban-column-height-fixer, - tg-bind-scope) - - div.kanban-task(ng-repeat="us in usByStatus[s.id] track by us.id", - tg-kanban-userstory, ng-model="us", tg-bind-scope, - tg-class-permission="{'readonly': '!modify_task'}" - ng-class="ctrl.getCardClass(s.id)") - + ng-repeat="s in usStatusList track by s.id", + tg-kanban-sortable, + tg-kanban-wip-limit="s", + tg-kanban-column-height-fixer, + tg-bind-scope + ) + div.kanban-task( + ng-repeat="us in usByStatus[s.id] track by us.id", + tg-kanban-userstory, + ng-model="us", + tg-bind-scope, + tg-class-permission="{'readonly': '!modify_task'}" + ng-class="{'kanban-task-maximized': ctrl.isMaximized(s.id), 'kanban-task-minimized': ctrl.isMinimized(s.id), 'card-placeholder': us.isPlaceholder}" + placeholder="{{us.isPlaceholder}}" + ) div.kanban-column-intro(ng-if="s.is_archived", tg-kanban-archived-status-intro="s") diff --git a/app/partials/includes/modules/lightbox-us-create-edit.jade b/app/partials/includes/modules/lightbox-us-create-edit.jade index 067d5c27..98259aa3 100644 --- a/app/partials/includes/modules/lightbox-us-create-edit.jade +++ b/app/partials/includes/modules/lightbox-us-create-edit.jade @@ -6,7 +6,7 @@ form input(type="text", name="subject", ng-model="us.subject", placeholder="{{'COMMON.FIELDS.SUBJECT' | translate}}", data-required="true", data-maxlength="500") - fieldset.estimation + fieldset.ticket-estimation tg-lb-us-estimation(ng-model="us") fieldset diff --git a/app/partials/includes/modules/login-form.jade b/app/partials/includes/modules/login-form.jade index 18bfa351..0d0fb609 100644 --- a/app/partials/includes/modules/login-form.jade +++ b/app/partials/includes/modules/login-form.jade @@ -1,17 +1,40 @@ div.login-form-container(tg-login) form.login-form fieldset - input(type="text", name="username", data-required="true", - placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_NAME' | translate}}") + input( + type="text" + name="username" + data-required="true" + placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_NAME' | translate}}" + ) + fieldset.login-password - input(type="password", name="password", data-required="true", - placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_PASSWORD' | translate}}") + input( + type="password" + name="password" + tg-capslock + data-required="true" + placeholder="{{'LOGIN_COMMON.PLACEHOLDER_AUTH_PASSWORD' | translate}}" + ) + // This should be hidden when focus on pass - a.forgot-pass(href="", tg-nav="forgot-password", title="{{'LOGIN_COMMON.TITLE_LINK_FORGOT_PASSWORD' | translate}}", translate="LOGIN_COMMON.LINK_FORGOT_PASSWORD") + a.forgot-pass( + href="" + tg-nav="forgot-password" + title="{{'LOGIN_COMMON.TITLE_LINK_FORGOT_PASSWORD' | translate}}" + translate="LOGIN_COMMON.LINK_FORGOT_PASSWORD" + ) fieldset - button.button-green.submit-button(type="submit", title="{{'LOGIN_COMMON.ACTION_SIGN_IN' | translate}}", translate="LOGIN_COMMON.ACTION_SIGN_IN") + button.button-green.submit-button( + type="submit" + title="{{'LOGIN_COMMON.ACTION_SIGN_IN' | translate}}" + translate="LOGIN_COMMON.ACTION_SIGN_IN" + ) - fieldset(ng-repeat="plugin in contribPlugins|filter:{type: 'auth'}", ng-include="plugin.template") + fieldset( + ng-repeat="plugin in contribPlugins|filter:{type: 'auth'}" + ng-include="plugin.template" + ) tg-public-register-message diff --git a/app/partials/includes/modules/register-form.jade b/app/partials/includes/modules/register-form.jade index 0b955507..621b26b5 100644 --- a/app/partials/includes/modules/register-form.jade +++ b/app/partials/includes/modules/register-form.jade @@ -1,31 +1,61 @@ div.register-form-container(tg-register) + form.register-form fieldset - input(type="text", name="username", ng-model="data.username", - data-required="true", data-maxlength="255", data-regexp="^[\\w.-]+$", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_NAME' | translate}}") + input( + type="text" + name="username" + ng-model="data.username" + data-required="true" + data-maxlength="255" + data-regexp="^[\\w.-]+$" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_NAME' | translate}}" + ) fieldset - input(type="text", name="full_name", ng-model="data.full_name", - data-required="true", data-maxlength="256", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_FULL_NAME' | translate}}") + input( + type="text" + name="full_name" + ng-model="data.full_name" + data-required="true" + data-maxlength="256" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_FULL_NAME' | translate}}" + ) fieldset - input(type="text", name="email", ng-model="data.email", - data-required="true", data-maxlength="255", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_EMAIL' | translate}}") + input( + type="text" + name="email" + ng-model="data.email" + data-required="true" + data-maxlength="255" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_EMAIL' | translate}}" + ) fieldset - input(type="password", name="password", ng-model="data.password", - data-required="true", data-minlength="4", - placeholder="{{'REGISTER_FORM.PLACEHOLDER_PASSWORD' | translate}}") + input( + type="password" + name="password" + tg-capslock + ng-model="data.password" + data-required="true" + data-minlength="4" + placeholder="{{'REGISTER_FORM.PLACEHOLDER_PASSWORD' | translate}}" + ) fieldset - button.button-register.button-gray.submit-button(type="submit", title="{{'REGISTER_FORM.ACTION_SIGN_UP' | translate}}", translate="REGISTER_FORM.ACTION_SIGN_UP") + button.button-register.button-gray.submit-button( + type="submit" + title="{{'REGISTER_FORM.ACTION_SIGN_UP' | translate}}" + translate="REGISTER_FORM.ACTION_SIGN_UP" + ) - fieldset(tg-github-login-button) + //- Only displays terms notice when terms plugin is loaded. + tg-terms-of-service-and-privacy-policy-notice - // Only displays terms notice when terms plugin is loaded. - tg-terms-notice - - a.register-text-top(href="", title="{{'REGISTER_FORM.TITLE_LINK_LOGIN' | translate}}", tg-nav="login", translate="REGISTER_FORM.LINK_LOGIN") + a.register-text-top( + href="" + title="{{'REGISTER_FORM.TITLE_LINK_LOGIN' | translate}}" + tg-nav="login" + translate="REGISTER_FORM.LINK_LOGIN" + ) diff --git a/app/partials/includes/modules/search-in.jade b/app/partials/includes/modules/search-in.jade index f121ff1e..e13718a3 100644 --- a/app/partials/includes/modules/search-in.jade +++ b/app/partials/includes/modules/search-in.jade @@ -2,4 +2,5 @@ section.search-in header fieldset input(type="text", placeholder="{{'SEARCH.PLACEHOLDER_SEARCH' | translate}}", ng-model="searchTerm") - a.icon.icon-search(href="", title="{{'SEARCH.TITLE_ACTION_SEARCH' | translate}}") + .icon-search-wrapper(tg-loading="loading") + a.icon.icon-search(href="", title="{{'SEARCH.TITLE_ACTION_SEARCH' | translate}}") diff --git a/app/partials/includes/modules/search-result-table.jade b/app/partials/includes/modules/search-result-table.jade index be95d40c..2394f9da 100644 --- a/app/partials/includes/modules/search-result-table.jade +++ b/app/partials/includes/modules/search-result-table.jade @@ -18,10 +18,8 @@ script(type="text/ng-template", id="search-issues") div.status(tg-listitem-issue-status="issue") div.assigned-to(tg-listitem-assignedto="issue") - div.empty.empty-search-results(ng-class="{'hidden': issues.length}") - span.icon.icon-issues - span.title(translate="SEARCH.EMPTY_TITLE") - span(translate="SEARCH.EMPTY_DESCRIPTION") + div.empty-search-results(ng-class="{'hidden': issues.length}") + include ../components/empty-search-results script(type="text/ng-template", id="search-userstories") @@ -42,10 +40,8 @@ script(type="text/ng-template", id="search-userstories") div.status(tg-listitem-us-status="us") div.points(tg-bo-bind="us.total_points") - div.empty.empty-search-results(ng-class="{'hidden': userstories.length}") - span.icon.icon-issues - span.title(translate="SEARCH.EMPTY_TITLE") - span(translate="SEARCH.EMPTY_DESCRIPTION") + div.empty-search-results(ng-class="{'hidden': userstories.length}") + include ../components/empty-search-results script(type="text/ng-template", id="search-tasks") div.search-result-table-container(ng-class="{'hidden': !tasks.length}", tg-bind-scope) @@ -65,10 +61,8 @@ script(type="text/ng-template", id="search-tasks") div.status(tg-listitem-task-status="task") div.assigned-to(tg-listitem-assignedto="task") - div.empty.empty-search-results(ng-class="{'hidden': tasks.length}") - span.icon.icon-issues - span.title(translate="SEARCH.EMPTY_TITLE") - span(translate="SEARCH.EMPTY_DESCRIPTION") + div.empty-search-results(ng-class="{'hidden': tasks.length}") + include ../components/empty-search-results script(type="text/ng-template", id="search-wikipages") div.search-result-table-container(ng-class="{'hidden': !wikipages.length}", tg-bind-scope) @@ -82,7 +76,5 @@ script(type="text/ng-template", id="search-wikipages") a(href="", tg-nav="project-wiki-page:project=project.slug,slug=wikipage.slug", tg-bo-bind="wikipage.slug") - div.empty.empty-search-results(ng-class="{'hidden': wikipages.length}") - span.icon.icon-issues - span.title(translate="SEARCH.EMPTY_TITLE") - span(translate="SEARCH.EMPTY_DESCRIPTION") + div.empty-search-results(ng-class="{'hidden': wikipages.length}") + include ../components/empty-search-results diff --git a/app/partials/includes/modules/sprint.jade b/app/partials/includes/modules/sprint.jade index 9812c258..29d229ac 100644 --- a/app/partials/includes/modules/sprint.jade +++ b/app/partials/includes/modules/sprint.jade @@ -3,7 +3,12 @@ header(tg-backlog-sprint-header, ng-model="sprint") div.sprint-progress-bar(tg-progress-bar="100 * sprint.closed_points / sprint.total_points") div.sprint-table - div.row.milestone-us-item-row(ng-repeat="us in sprint.user_stories track by us.id", tg-bind-scope, tg-class-permission="{'readonly': '!modify_us'}") + div.sprint-empty(ng-if="!sprint.user_stories.length") {{ 'BACKLOG.SPRINTS.WARNING_EMPTY_SPRINT' | translate }} + div.row.milestone-us-item-row( + ng-repeat="us in sprint.user_stories track by us.id" + tg-bind-scope + tg-class-permission="{'readonly': '!modify_us'}" + ) div.column-us a.us-name.clickable(tg-nav="project-userstories-detail:project=project.slug,ref=us.ref", tg-nav-get-params="{\"milestone\": {{us.milestone}}}" diff --git a/app/partials/includes/modules/sprints.jade b/app/partials/includes/modules/sprints.jade index 4e16e493..3324f818 100644 --- a/app/partials/includes/modules/sprints.jade +++ b/app/partials/includes/modules/sprints.jade @@ -1,13 +1,32 @@ section.sprints - header - h1(translate="BACKLOG.SPRINTS.TITLE") - div.summary - div.total-sprints - span.number(ng-bind="project.total_milestones") -- - span.description(translate="BACKLOG.SPRINTS.NUMBER_SPRINTS") - a.button-green.add-sprint(href="", title="{{ 'BACKLOG.SPRINTS.TITLE_ACTION_NEW_SPRINT' | translate }}", - ng-click="ctrl.addNewSprint()", tg-check-permission="add_milestone") - span.text(translate="BACKLOG.SPRINTS.ACTION_NEW_SPRINT") + header.sprint-header + h1 + span.number( + ng-bind="totalMilestones" + ng-if="totalMilestones" + ) + span(translate="BACKLOG.SPRINTS.TITLE") + a.add-sprint( + href="" + title="{{ 'BACKLOG.SPRINTS.TITLE_ACTION_NEW_SPRINT' | translate}}" + ng-click="ctrl.addNewSprint()" + ng-if="totalMilestones" + tg-check-permission="add_milestone" + ) + include ../../../svg/add.svg + + div.sprints-empty(ng-if="!totalMilestones") + img( + src="/images/sprint-empty.png" + alt="{{'BACKLOG.SPRINTS.EMPTY' | translate}}" + ) + p.title(translate="BACKLOG.SPRINTS.EMPTY") + a( + href="" + ng-click="ctrl.addNewSprint()" + title="{{'BACKLOG.SPRINTS.TITLE_ACTION_NEW_SPRINT' | translate}}" + translate="BACKLOG.SPRINTS.TEXT_ACTION_NEW_SPRINT" + ) div.sprint.sprint-open(ng-repeat="sprint in openSprints track by sprint.id", tg-backlog-sprint="sprint", @@ -15,7 +34,7 @@ section.sprints include sprint a.filter-closed-sprints(href="", tg-backlog-toggle-closed-sprints-visualization, - ng-show="totalClosedMilestones") + ng-if="totalClosedMilestones") span.icon.icon-archive span.text(translate="BACKLOG.SPRINTS.ACTION_SHOW_CLOSED_SPRINTS") diff --git a/app/partials/includes/modules/taskboard-table.jade b/app/partials/includes/modules/taskboard-table.jade index e3163ba9..de30e250 100644 --- a/app/partials/includes/modules/taskboard-table.jade +++ b/app/partials/includes/modules/taskboard-table.jade @@ -24,11 +24,21 @@ div.taskboard-table(tg-taskboard-squish-column) span(ng-bind="us.total_points") span(translate="TASKBOARD.TABLE.FIELD_POINTS") include ../components/addnewtask + div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", tg-taskboard-sortable, class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope) - div.taskboard-task(ng-repeat="task in usTasks[us.id][st.id] track by task.id", - tg-taskboard-task, tg-bind-scope, tg-class-permission="{'readonly': '!modify_task'}") + div.taskboard-task( + ng-repeat="task in usTasks[us.id][st.id] track by task.id" + tg-bind-scope + tg-class-permission="{'readonly': '!modify_task'}" + ng-class="{'card-placeholder': task.isPlaceholder}" + ) + div(ng-if="!task.isPlaceholder", tg-taskboard-task) include ../components/taskboard-task + div(ng-if="task.isPlaceholder") + - var card = 'task' + include ../../common/components/taskboard-placeholder + div.task-row(ng-init="us = null", ng-class="{'row-fold':usFolded[null]}") div.taskboard-userstory-box.task-column a.icon.icon-vfold.vfold(href="", title="{{'TASKBOARD.TABLE.TITLE_ACTION_FOLD_ROW' | translate}}", ng-click='foldUs()', ng-class="{hidden:usFolded[null]}") @@ -37,6 +47,15 @@ div.taskboard-table(tg-taskboard-squish-column) span(translate="TASKBOARD.TABLE.ROW_UNASSIGED_TASKS_TITLE") include ../components/addnewtask.jade div.taskboard-tasks-box.task-column(ng-repeat="st in taskStatusList track by st.id", tg-taskboard-sortable, class="squish-status-{{st.id}}", ng-class="{'column-fold':statusesFolded[st.id]}", tg-bind-scope) - div.taskboard-task(ng-repeat="task in usTasks[null][st.id] track by task.id", - tg-taskboard-task, tg-bind-scope, tg-class-permission="{'readonly': '!modify_task'}") - include ../components/taskboard-task + div.taskboard-task( + ng-repeat="task in usTasks[null][st.id] track by task.id" + tg-taskboard-task + tg-bind-scope + tg-class-permission="{'readonly': '!modify_task'}" + ng-class="{'card-placeholder': task.isPlaceholder}" + ) + div(ng-if="!task.isPlaceholder") + include ../components/taskboard-task + + div(ng-if="task.isPlaceholder") + include ../../common/components/taskboard-placeholder diff --git a/app/partials/includes/modules/team/team-filters.jade b/app/partials/includes/modules/team/team-filters.jade index bd0fe677..d2d68cb9 100644 --- a/app/partials/includes/modules/team/team-filters.jade +++ b/app/partials/includes/modules/team/team-filters.jade @@ -6,6 +6,7 @@ section.team-filters form.search-in fieldset input(type="text", placeholder="{{'TEAM.PLACEHOLDER_INPUT_SEARCH' | translate}}", ng-model="filtersQ") - a.icon.icon-search(href="", title="{{'COMMON.FILTERS.TITLE_ACTION_FILTER_BUTTON' | translate}}") + .icon-search-wrapper + a.icon.icon-search(href="", title="{{'COMMON.FILTERS.TITLE_ACTION_FILTER_BUTTON' | translate}}") nav(tg-team-filters) \ No newline at end of file diff --git a/app/partials/issue/issues-detail.jade b/app/partials/issue/issues-detail.jade index c4f1d6d5..397bb6fc 100644 --- a/app/partials/issue/issues-detail.jade +++ b/app/partials/issue/issues-detail.jade @@ -1,74 +1,115 @@ doctype html -div.wrapper(ng-controller="IssueDetailController as ctrl", - ng-init="section='issues'") +div.wrapper( + ng-controller="IssueDetailController as ctrl", + ng-init="section='issues'" +) tg-project-menu + div.main.us-detail div.us-detail-header.header-with-actions include ../includes/components/mainTitle section.us-story-main-data - div.us-title(ng-class="{blocked: issue.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="issue.ref") - span.us-name(tg-editable-subject, ng-model="issue", required-perm="modify_issue") + header + tg-vote-button.upvote-btn( + item="issue" + on-upvote="ctrl.onUpvote" + on-downvote="ctrl.onDownvote" + ) + .us-title(ng-class="{blocked: issue.is_blocked}") + h2.us-title-text + span.us-number(tg-bo-ref="issue.ref") + span.us-name(tg-editable-subject, ng-model="issue", required-perm="modify_issue") - p.us-related-task(ng-if="issue.generated_user_stories.length") - | {{ 'ISSUES.PROMOTED'|translate }} - a(ng-repeat="us in issue.generated_user_stories", - tg-check-permission="view_us", href="", - tg-bo-title="'#' + us.ref + ' ' + us.subject", - tg-nav="project-userstories-detail:project=project.slug,ref=us.ref") - span(tg-bo-ref="us.ref") + p.us-related-task(ng-if="issue.generated_user_stories.length") + | {{ 'ISSUES.PROMOTED'|translate }} + a( + href="" + ng-repeat="us in issue.generated_user_stories" + tg-check-permission="view_us" + tg-bo-title="'#' + us.ref + ' ' + us.subject" + tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" + ) + span(tg-bo-ref="us.ref") - p.external-reference(ng-if="issue.external_reference") - | {{ 'ISSUES.EXTERNAL_REFERENCE'|translate }} - a(target="_blank", tg-bo-href="issue.external_reference[1]", - title="{{'ISSUES.GO_TO_EXTERNAL_REFERENCE' | translate}}") - span {{ issue.external_reference[1] }} + p.external-reference(ng-if="issue.external_reference") + | {{ 'ISSUES.EXTERNAL_REFERENCE'|translate }} + a( + target="_blank" + tg-bo-href="issue.external_reference[1]" + title="{{'ISSUES.GO_TO_EXTERNAL_REFERENCE' | translate}}" + ) + span {{ issue.external_reference[1] }} - p.block-desc-container(ng-show="issue.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="issue.blocked_note || ('ISSUES.BLOCKED' | translate)") + p.block-desc-container(ng-show="issue.is_blocked") + span.block-description-title(translate="COMMON.BLOCKED") + span.block-description(ng-bind="issue.blocked_note || ('ISSUES.BLOCKED' | translate)") - div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", - title="{{'ISSUES.TITLE_PREVIOUS_ISSUE' | translate}}") - a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", - title="{{'ISSUES.TITLE_NEXT_ISSUE' | translate}}") + div.issue-nav + a.icon.icon-arrow-left( + ng-show="previousUrl" + tg-bo-href="previousUrl" + title="{{'ISSUES.TITLE_PREVIOUS_ISSUE' | translate}}" + ) + a.icon.icon-arrow-right( + ng-show="nextUrl" + tg-bo-href="nextUrl" + title="{{'ISSUES.TITLE_NEXT_ISSUE' | translate}}" + ) div.tags-block(tg-tag-line, ng-model="issue", required-perm="modify_issue") section.duty-content(tg-editable-description, ng-model="issue", required-perm="modify_issue") // Custom Fields - tg-custom-attributes-values(ng-model="issue", type="issue", project="project", required-edition-perm="modify_issue") + tg-custom-attributes-values( + ng-model="issue" + type="issue" + project="project" + required-edition-perm="modify_issue" + ) tg-attachments(ng-model="issue", type="issue") tg-history(ng-model="issue", type="issue") - sidebar.menu-secondary.sidebar - section.us-status - h1(tg-issue-status-display, ng-model="issue") - tg-created-by-display.us-created-by(ng-model="issue") - div.duty-data-container - div.duty-data(tg-issue-type-button, ng-model="issue") - div.duty-data(tg-issue-severity-button, ng-model="issue") - div.duty-data(tg-issue-priority-button, ng-model="issue") - div.duty-data(tg-issue-status-button, ng-model="issue") + sidebar.menu-secondary.sidebar.ticket-data + section.status + .ticket-title(tg-issue-status-display, ng-model="issue") + tg-created-by-display.ticket-created-by(ng-model="issue") + div.ticket-data-container + div.ticket-status(tg-issue-type-button, ng-model="issue") + div.ticket-status(tg-issue-severity-button, ng-model="issue") + div.ticket-status(tg-issue-priority-button, ng-model="issue") + div.ticket-status(tg-issue-status-button, ng-model="issue") - section.duty-assigned-to(tg-assigned-to, ng-model="issue", required-perm="modify_issue") + section.ticket-assigned-to(tg-assigned-to, ng-model="issue", required-perm="modify_issue") - section.watchers(tg-watchers, ng-model="issue", required-perm="modify_issue") + section.track-buttons-container.ticket-track-buttons - section.us-detail-settings - tg-promote-issue-to-us-button(tg-check-permission="add_us", ng-model="issue") - tg-block-button(tg-check-permission="modify_issue", ng-model="issue") - tg-delete-button(tg-check-permission="delete_issue", - on-delete-title="{{'ISSUES.ACTION_DELETE' | translate}}", - on-delete-go-to-url="onDeleteGoToUrl", - ng-model="issue") + div.watch-button + tg-watch-button( + item="issue" + on-watch="ctrl.onWatch" + on-unwatch="ctrl.onUnwatch" + ) - div.lightbox.lightbox-block(tg-lb-block, title="ISSUES.LIGHTBOX_TITLE_BLOKING_ISSUE", ng-model="issue") + div.ticket-watchers( + tg-watchers + ng-model="issue" + required-perm="modify_issue" + ) + + section.ticket-detail-settings + tg-promote-issue-to-us-button(tg-check-permission="add_us", ng-model="issue") + tg-block-button(tg-check-permission="modify_issue", ng-model="issue") + tg-delete-button( + tg-check-permission="delete_issue", + on-delete-title="{{'ISSUES.ACTION_DELETE' | translate}}", + on-delete-go-to-url="onDeleteGoToUrl", + ng-model="issue" + ) + + div.lightbox.lightbox-block(tg-lb-block, ng-model="issue", title="ISSUES.LIGHTBOX_TITLE_BLOKING_ISSUE") div.lightbox.lightbox-select-user(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/issue/issues.jade b/app/partials/issue/issues.jade index 8073ca4a..cee3d14a 100644 --- a/app/partials/issue/issues.jade +++ b/app/partials/issue/issues.jade @@ -9,7 +9,7 @@ div.wrapper.issues(tg-issues, ng-controller="IssuesController as ctrl", ng-init= header include ../includes/components/mainTitle - include ../includes/modules/list-filters + include ../includes/modules/issues-options include ../includes/modules/issues-table // Paginator is rendered using js. diff --git a/app/partials/kanban/kanban-task.jade b/app/partials/kanban/kanban-task.jade index e108a4ab..a63842bc 100644 --- a/app/partials/kanban/kanban-task.jade +++ b/app/partials/kanban/kanban-task.jade @@ -1,4 +1,8 @@ -div.kanban-tagline(tg-colorize-tags="us.tags", tg-colorize-tags-type="kanban", ng-hide="us.isArchived") +div.kanban-tagline( + tg-colorize-tags="us.tags" + tg-colorize-tags-type="kanban" + ng-hide="us.isArchived" +) div.kanban-task-inner(ng-class="{'task-archived': us.isArchived}") div.avatar-wrapper(tg-kanban-user-avatar="us.assigned_to", ng-model="us", ng-hide="us.isArchived") div.task-text(ng-hide="us.isArchived") diff --git a/app/partials/project/wizard-create-project.jade b/app/partials/project/wizard-create-project.jade index 9e61a345..1d52dc48 100644 --- a/app/partials/project/wizard-create-project.jade +++ b/app/partials/project/wizard-create-project.jade @@ -10,7 +10,7 @@ form ng-value='template.id', ng-model="data.creation_template", data-required="true") label.backlog(for="template-{{ template.id }}") - span.icon.icon-backlog + span.icon( ng-class="'icon-'+template.slug") h2 {{ template.name }} p {{ template.description }} diff --git a/app/partials/task/related-task-create-form.jade b/app/partials/task/related-task-create-form.jade index 03b822a5..35f0f4f9 100644 --- a/app/partials/task/related-task-create-form.jade +++ b/app/partials/task/related-task-create-form.jade @@ -6,7 +6,7 @@ a.icon.icon-delete.cancel-edit(href='', title="{{'COMMON.CANCEL' | translate}}") .status(tg-related-task-status='newTask', ng-model='newTask', not-auto-save='true') - a.task-status(href='', title="{{'TITLE_SELECT_STATUS' | translate}}") + a.task-status(href='', title="{{'TASK.TITLE_SELECT_STATUS' | translate}}") span.task-status-bind span.icon.icon-arrow-bottom diff --git a/app/partials/task/related-task-row-edit.jade b/app/partials/task/related-task-row-edit.jade index 8016c273..6023b2ae 100644 --- a/app/partials/task/related-task-row-edit.jade +++ b/app/partials/task/related-task-row-edit.jade @@ -6,7 +6,7 @@ a.icon.icon-delete.cancel-edit(href='', title="{{'COMMON.CANCEL' | translate}}") .status(tg-related-task-status='task', ng-model='task') - a.task-status(href='', title="{{'COMON.TITLE_SELECT_STATUS' | translate}}") + a.task-status(href='', title="{{'TASK.TITLE_SELECT_STATUS' | translate}}") span.task-status-bind span.icon.icon-arrow-bottom diff --git a/app/partials/task/task-detail.jade b/app/partials/task/task-detail.jade index e24dd306..1f732574 100644 --- a/app/partials/task/task-detail.jade +++ b/app/partials/task/task-detail.jade @@ -1,76 +1,130 @@ doctype html -div.wrapper(ng-controller="TaskDetailController as ctrl", - ng-init="section='backlog-kanban'") +div.wrapper( + ng-controller="TaskDetailController as ctrl" + ng-init="section='backlog-kanban'" +) tg-project-menu + div.main.us-detail div.us-detail-header.header-with-actions include ../includes/components/mainTitle .action-buttons a.button-gray( - tg-check-permission="view_milestones", - href="", title="{{'TASK.TITLE_LINK_TASKBOARD' | translate}}", - tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", - ng-if="sprint && project.is_backlog_activated", translate="TASK.LINK_TASKBOARD") + href="" + title="{{'TASK.TITLE_LINK_TASKBOARD' | translate}}" + tg-check-permission="view_milestones" + tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug" + ng-if="sprint && project.is_backlog_activated" + translate="TASK.LINK_TASKBOARD" + ) section.us-story-main-data - div.us-title(ng-class="{blocked: task.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="task.ref") - span.us-name(tg-editable-subject, ng-model="task", required-perm="modify_task") + header + tg-vote-button.upvote-btn( + item="task", + on-upvote="ctrl.onUpvote", + on-downvote="ctrl.onDownvote" + ) + div.us-title(ng-class="{blocked: task.is_blocked}") + h2.us-title-text + span.us-number(tg-bo-ref="task.ref") + span.us-name( + tg-editable-subject + ng-model="task" + required-perm="modify_task" + ) - h3.us-related-task(ng-if="us") - | {{ 'TASK.OWNER_US'|translate }} - a(tg-check-permission="view_us", href="", title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}", - tg-nav="project-userstories-detail:project=project.slug,ref=us.ref") - span(tg-bo-ref="us.ref") - span(tg-bo-bind="us.subject") + h3.us-related-task(ng-if="us") + | {{ 'TASK.OWNER_US'|translate }} + a( + href="" + tg-check-permission="view_us" + tg-nav="project-userstories-detail:project=project.slug,ref=us.ref" + title="{{'TASK.TITLE_LINK_GO_OWNER' | translate}}" + ) + span(tg-bo-ref="us.ref") + span(tg-bo-bind="us.subject") - p.external-reference(ng-if="task.external_reference") - a(target="_blank", tg-bo-href="task.external_reference[1]", - title="{{'TASK.TITLE_LINK_GO_ORIGIN' | translate}}") - | {{ "TASK.ORIGIN_US"| translate }} - span {{ task.external_reference[1] }} + p.external-reference(ng-if="task.external_reference") + a( + tg-bo-href="task.external_reference[1]", + target="_blank" + title="{{'TASK.TITLE_LINK_GO_ORIGIN' | translate}}" + ) + | {{ "TASK.ORIGIN_US"| translate }} + span {{ task.external_reference[1] }} - p.block-desc-container(ng-show="task.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="task.blocked_note || ('TASK.BLOCKED_DESCRIPTION' | translate)") + p.block-desc-container(ng-show="task.is_blocked") + span.block-description-title(translate="COMMON.BLOCKED") + span.block-description( + ng-bind="task.blocked_note || ('TASK.BLOCKED_DESCRIPTION' | translate)" + ) - div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", - title="{{'TASK.PREVIOUS' | translate}}") - a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", - title="{{'TASK.NEXT' | translate}}") + div.issue-nav + a.icon.icon-arrow-left( + ng-show="previousUrl" + tg-bo-href="previousUrl" + title="{{'TASK.PREVIOUS' | translate}}" + ) + a.icon.icon-arrow-right( + ng-show="nextUrl" + tg-bo-href="nextUrl" + title="{{'TASK.NEXT' | translate}}" + ) div.tags-block(tg-tag-line, ng-model="task", required-perm="modify_task") section.duty-content(tg-editable-description, ng-model="task", required-perm="modify_task") // Custom Fields - tg-custom-attributes-values(ng-model="task", type="task", project="project", required-edition-perm="modify_task") + tg-custom-attributes-values( + ng-model="task" + type="task" + project="project" + required-edition-perm="modify_task" + ) tg-attachments(ng-model="task", type="task") tg-history(ng-model="task", type="task") - sidebar.menu-secondary.sidebar - section.us-status - h1(tg-task-status-display, ng-model="task") - div.us-created-by(tg-created-by-display, ng-model="task") - div.duty-data-container - div.duty-data(tg-task-status-button, ng-model="task") + sidebar.menu-secondary.sidebar.ticket-data - section.duty-assigned-to(tg-assigned-to, ng-model="task", required-perm="modify_task") + section.status - section.watchers(tg-watchers, ng-model="task", required-perm="modify_task") + .ticket-title(tg-task-status-display, ng-model="task") - section.us-detail-settings - tg-task-is-iocaine-button(ng-model="task") - tg-block-button(tg-check-permission="modify_task", ng-model="task") - tg-delete-button(tg-check-permission="delete_task", - on-delete-title="{{'TASK.TITLE_DELETE_ACTION' | translate}}", - on-delete-go-to-url="onDeleteGoToUrl", - ng-model="task") + .ticket-created-by(tg-created-by-display, ng-model="task") - div.lightbox.lightbox-block(tg-lb-block, title="TASK.LIGHTBOX_TITLE_BLOKING_TASK", ng-model="task") + .ticket-data-container + .ticket-status(tg-task-status-button, ng-model="task") + + section.ticket-assigned-to(tg-assigned-to, ng-model="task", required-perm="modify_task") + + section.track-buttons-container.ticket-track-buttons + div.watch-button + tg-watch-button( + item="task" + on-watch="ctrl.onWatch" + on-unwatch="ctrl.onUnwatch" + ) + + div.ticket-watchers( + tg-watchers, + ng-model="task", + required-perm="modify_task" + ) + + section.ticket-detail-settings + tg-task-is-iocaine-button(ng-model="task") + tg-block-button(tg-check-permission="modify_task", ng-model="task") + tg-delete-button( + tg-check-permission="delete_task" + on-delete-title="{{'TASK.TITLE_DELETE_ACTION' | translate}}" + on-delete-go-to-url="onDeleteGoToUrl" + ng-model="task" + ) + + div.lightbox.lightbox-block(tg-lb-block, ng-model="task", title="TASK.LIGHTBOX_TITLE_BLOKING_TASK") div.lightbox.lightbox-select-user(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/team/team-member-current-user.jade b/app/partials/team/team-member-current-user.jade index 698c2f7f..b5d81ce1 100644 --- a/app/partials/team/team-member-current-user.jade +++ b/app/partials/team/team-member-current-user.jade @@ -1,10 +1,10 @@ .row .username figure.avatar - img(tg-bo-src="currentUser.photo", tg-bo-alt="currentUser.full_name") + img(tg-bo-src="currentUser.photo", tg-bo-alt="currentUser.full_name_display") figcaption - span.name(tg-bo-bind="currentUser.full_name") + span.name(tg-bo-bind="currentUser.full_name_display") span.position(tg-bo-bind="currentUser.role_name") diff --git a/app/partials/team/team-members.jade b/app/partials/team/team-members.jade index b941b9d7..55df4055 100644 --- a/app/partials/team/team-members.jade +++ b/app/partials/team/team-members.jade @@ -1,10 +1,11 @@ .row.member(ng-repeat="user in memberships | membersFilter:filtersQ:filtersRole") .username figure.avatar - img(tg-bo-src="user.photo", tg-bo-alt="user.full_name") + img(tg-bo-src="user.photo", tg-bo-alt="user.full_name_display") figcaption - a.name(tg-nav="user-profile:username=user.username", title="{{::user.full_name }}") {{::user.full_name}} + a.name(tg-nav="user-profile:username=user.username", + title="{{::user.full_name_display}}") {{::user.full_name_display}} span.position {{::user.role_name}} .member-stats(tg-team-member-stats, stats="stats", diff --git a/app/partials/us/us-detail.jade b/app/partials/us/us-detail.jade index 21eaa11a..f3606df8 100644 --- a/app/partials/us/us-detail.jade +++ b/app/partials/us/us-detail.jade @@ -1,80 +1,152 @@ doctype html -div.wrapper(ng-controller="UserStoryDetailController as ctrl", - ng-init="section='backlog-kanban'") +div.wrapper( + ng-controller="UserStoryDetailController as ctrl", + ng-init="section='backlog-kanban'" +) tg-project-menu + div.main.us-detail div.us-detail-header.header-with-actions include ../includes/components/mainTitle .action-buttons a.button-gray( - tg-check-permission="view_milestones", - href="", title="{{'US.TITLE_LINK_TASKBOARD' | translate}}", - tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug", - ng-if="sprint && project.is_backlog_activated", translate="US.LINK_TASKBOARD") + href="" + tg-check-permission="view_milestones" + tg-nav="project-taskboard:project=project.slug,sprint=sprint.slug" + ng-if="sprint && project.is_backlog_activated" + title="{{'US.TITLE_LINK_TASKBOARD' | translate}}" + translate="US.LINK_TASKBOARD" + ) section.us-story-main-data - div.us-title(ng-class="{blocked: us.is_blocked}") - h2.us-title-text - span.us-number(tg-bo-ref="us.ref") - span.us-name(tg-editable-subject, ng-model="us", required-perm="modify_us") + header + tg-vote-button.upvote-btn( + item="us" + on-upvote="ctrl.onUpvote" + on-downvote="ctrl.onDownvote" + ) + div.us-title(ng-class="{blocked: us.is_blocked}") + h2.us-title-text + span.us-number(tg-bo-ref="us.ref") + span.us-name(tg-editable-subject, ng-model="us", required-perm="modify_us") - p.us-related-task(ng-if="us.origin_issue") - | {{ 'US.PROMOTED'|translate }} - a(tg-check-permission="view_us", href="", title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}", - tg-nav="project-issues-detail:project=project.slug,ref=us.origin_issue.ref" - tg-bo-title="'#' + us.origin_issue.ref + ' ' + us.origin_issue.subject") - span(tg-bo-ref="us.origin_issue.ref") + p.us-related-task(ng-if="us.origin_issue") + | {{ 'US.PROMOTED'|translate }} + a( + href="" + tg-check-permission="view_us" + tg-nav="project-issues-detail:project=project.slug,ref=us.origin_issue.ref" + tg-bo-title="'#' + us.origin_issue.ref + ' ' + us.origin_issue.subject" + title="{{'US.TITLE_LINK_GO_TO_ISSUE' | translate}}" + ) + span(tg-bo-ref="us.origin_issue.ref") - p.external-reference(ng-if="us.external_reference") - | {{ 'US.EXTERNAL_REFERENCE'|translate }} - a(target="_blank", tg-bo-href="us.external_reference[1]", - title="{{'US.GO_TO_EXTERNAL_REFERENCE' | translate}}") - span {{ us.external_reference[1] }} + p.external-reference(ng-if="us.external_reference") + | {{ 'US.EXTERNAL_REFERENCE'|translate }} + a( + tg-bo-href="us.external_reference[1]", + title="{{'US.GO_TO_EXTERNAL_REFERENCE' | translate}}" + target="_blank" + ) + span {{ us.external_reference[1] }} - p.block-desc-container(ng-show="us.is_blocked") - span.block-description-title(translate="COMMON.BLOCKED") - span.block-description(ng-bind="us.blocked_note || ('US.BLOCKED' | translate)") - div.issue-nav - a.icon.icon-arrow-left(ng-show="previousUrl", tg-bo-href="previousUrl", - title="{{'US.PREVIOUS' | translate}}") - a.icon.icon-arrow-right(ng-show="nextUrl", tg-bo-href="nextUrl", - title="{{'US.NEXT' | translate}}") + p.block-desc-container(ng-show="us.is_blocked") + span.block-description-title(translate="COMMON.BLOCKED") + span.block-description(ng-bind="us.blocked_note || ('US.BLOCKED' | translate)") + div.issue-nav + a.icon.icon-arrow-left( + ng-show="previousUrl" + tg-bo-href="previousUrl" + title="{{'US.PREVIOUS' | translate}}" + ) + a.icon.icon-arrow-right( + ng-show="nextUrl" + tg-bo-href="nextUrl" + title="{{'US.NEXT' | translate}}" + ) div.tags-block(tg-tag-line, ng-model="us", required-perm="modify_us") section.duty-content(tg-editable-description, ng-model="us", required-perm="modify_us") // Custom Fields - tg-custom-attributes-values(ng-model="us", type="userstory", project="project", required-edition-perm="modify_us") + tg-custom-attributes-values( + ng-model="us" + type="userstory" + project="project" + required-edition-perm="modify_us" + ) include ../includes/modules/related-tasks - tg-attachments(ng-model="us", type="us") - tg-history(ng-model="us", type="us") + tg-attachments( + ng-model="us" + type="us" + ) + tg-history( + ng-model="us" + type="us" + ) - sidebar.menu-secondary.sidebar - section.us-status - h1(tg-us-status-display, ng-model="us") - div.us-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks") - tg-created-by-display.us-created-by(ng-model="us") + sidebar.menu-secondary.sidebar.ticket-data + section + div.ticket-title( + tg-us-status-display + ng-model="us" + ) + + tg-created-by-display.ticket-created-by(ng-model="us") + + //div.ticket-detail-progress-bar(tg-us-tasks-progress-display, ng-model="tasks") + + div.ticket-data-container + div.ticket-status( + tg-us-status-button + ng-model="us" + ) + + section.ticket-estimation tg-us-estimation(ng-model="us") - div.duty-data-container - div.duty-data(tg-us-status-button, ng-model="us") - section.duty-assigned-to(tg-assigned-to, ng-model="us", required-perm="modify_us") + section.ticket-assigned-to( + tg-assigned-to + ng-model="us" + required-perm="modify_us" + ) - section.watchers(tg-watchers, ng-model="us", required-perm="modify_us") + section.track-buttons-container.ticket-track-buttons + div.watch-button + tg-watch-button( + item="us" + on-watch="ctrl.onWatch" + on-unwatch="ctrl.onUnwatch" + ) - section.us-detail-settings + div.ticket-watchers( + tg-watchers + ng-model="us" + required-perm="modify_us" + ) + + section.ticket-detail-settings tg-us-team-requirement-button(ng-model="us") tg-us-client-requirement-button(ng-model="us") - tg-block-button(tg-check-permission="modify_us", ng-model="us") - tg-delete-button(tg-check-permission="delete_us", - on-delete-title="{{'Delete User Story' | translate}}", - on-delete-go-to-url="onDeleteGoToUrl", - ng-model="us") + tg-block-button( + tg-check-permission="modify_us" + ng-model="us" + ) + tg-delete-button( + tg-check-permission="delete_us" + on-delete-title="{{'Delete User Story' | translate}}" + on-delete-go-to-url="onDeleteGoToUrl" + ng-model="us" + ) - div.lightbox.lightbox-block(tg-lb-block, title="US.LIGHTBOX_TITLE_BLOKING_US", ng-model="us") + div.lightbox.lightbox-block( + tg-lb-block + title="{{ 'US.LIGHTBOX_TITLE_BLOKING_US' | translate }}" + ng-model="us" + ) div.lightbox.lightbox-select-user(tg-lb-assignedto) div.lightbox.lightbox-select-user(tg-lb-watchers) diff --git a/app/partials/user/lightbox/lightbox-delete-account.jade b/app/partials/user/lightbox/lightbox-delete-account.jade index 9d4fb190..5befed76 100644 --- a/app/partials/user/lightbox/lightbox-delete-account.jade +++ b/app/partials/user/lightbox/lightbox-delete-account.jade @@ -3,10 +3,10 @@ a.close(href="", title="{{'close' | translate}}") form h2.title(translate="LIGHTBOX.DELETE_ACCOUNT.SECTION_NAME") p - span.question(translate="LIGHBOX.DELETE_ACCOUNT.CONFIRM") - span.subtitle(translate="LIGHBOX.DELETE_ACCOUNT.SUBTITLE") + span.question(translate="LIGHTBOX.DELETE_ACCOUNT.CONFIRM") + span.subtitle(translate="LIGHTBOX.DELETE_ACCOUNT.SUBTITLE") div.options a.button-green(href="", title="{{'COMMON.ACCEPT' | translate}}") - span(translate="COMMON.ACCEPT") Accept + span(translate="COMMON.ACCEPT") a.button-red(href="", title="{{'Cancel' | translate}}", ) - span(translate="COMMON.CANCEL") Accept + span(translate="COMMON.CANCEL") diff --git a/app/partials/user/user-change-password.jade b/app/partials/user/user-change-password.jade index f2d6df51..734005c9 100644 --- a/app/partials/user/user-change-password.jade +++ b/app/partials/user/user-change-password.jade @@ -1,7 +1,10 @@ doctype html -div.wrapper(tg-user-change-password, ng-controller="UserChangePasswordController as ctrl", - ng-init="section='user-settings'") +div.wrapper( + tg-user-change-password + 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") @@ -15,12 +18,40 @@ div.wrapper(tg-user-change-password, ng-controller="UserChangePasswordController form fieldset label(for="current-password", translate="CHANGE_PASSWORD.FIELD_CURRENT_PASSWORD") - input(type="password", placeholder="{{'CHANGE_PASSWORD.PLACEHOLDER_CURRENT_PASSWORD' | translate}}", id="current-password", ng-model="currentPassword") + input( + type="password" + name="password" + id="current-password" + tg-capslock + ng-model="currentPassword" + placeholder="{{'CHANGE_PASSWORD.PLACEHOLDER_CURRENT_PASSWORD' | translate}}" + ) + fieldset label(for="new-password", translate="CHANGE_PASSWORD.FIELD_NEW_PASSWORD") - input(type="password", placeholder="{{'CHANGE_PASSWORD.PLACEHOLDER_NEW_PASSWORD' | translate}}", id="new-password", ng-model="newPassword1") + input( + type="password" + name="new-password" + id="new-password" + tg-capslock + ng-model="newPassword1" + placeholder="{{'CHANGE_PASSWORD.PLACEHOLDER_NEW_PASSWORD' | translate}}" + ) + fieldset label(for="retype-password", translate="CHANGE_PASSWORD.FIELD_RETYPE_PASSWORD") - input(type="password", placeholder="{{'CHANGE_PASSWORD.PLACEHOLDER_RETYPE_PASSWORD' | translate}}", id="retype-password", ng-model="newPassword2") + input( + type="password" + name="retype-password" + id="retype-password" + tg-capslock + ng-model="newPassword2" + placeholder="{{'CHANGE_PASSWORD.PLACEHOLDER_RETYPE_PASSWORD' | translate}}" + ) + fieldset - button.button-green.submit-button(type="submit", title="{{'COMMON.SAVE' | translate}}", translate="COMMON.SAVE") + button.button-green.submit-button( + type="submit" + title="{{'COMMON.SAVE' | translate}}" + translate="COMMON.SAVE" + ) diff --git a/app/partials/user/user-profile.jade b/app/partials/user/user-profile.jade index 872b6e79..4e60adf1 100644 --- a/app/partials/user/user-profile.jade +++ b/app/partials/user/user-profile.jade @@ -18,55 +18,107 @@ div.wrapper(tg-user-profile, ng-controller="UserSettingsController as ctrl", .image-container img.avatar(ng-src="{{user.big_photo}}" alt="avatar") .overlay.hidden - img.loading-spinner(src="/svg/spinner-circle.svg", - alt="{{'COMMON.LOADING' | translate}}") - input(type="file", id="avatar-field", class="hidden", - tg-avatar-model="avatarAttachment") + img.loading-spinner( + src="/svg/spinner-circle.svg", + alt="{{'COMMON.LOADING' | translate}}" + ) + + input.hidden( + type="file" + id="avatar-field" + tg-avatar-model="avatarAttachment" + ) + p(translate="USER_PROFILE.IMAGE_HELP") span.size-info.hidden(tg-bo-html="maxFileSizeMsg") - a.button-green.change.js-change-avatar(translate="USER_PROFILE.ACTION_CHANGE_IMAGE", - title="{{'USER_PROFILE.CHANGE_PHOTO' | translate}} {{maxFileSizeMsg}}") + + 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") div.data fieldset - label(for="email", translate="USER_PROFILE.FIELD.USERNAME") - input(type="text", name="username", id="username", - placeholder="{{'USER_PROFILE.FIELD.USERNAME' | translate}}", - ng-model="user.username", data-required="true", data-maxlength="255", - data-regexp="^[\\w.-]+$") + label(for="username", translate="USER_PROFILE.FIELD.USERNAME") + input( + type="text" + 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="text", name="email", id="email", - placeholder="{{'USER_PROFILE.FIELD.EMAIL' | translate}}", - ng-model="user.email", data-type="email", data-required="true", - data-maxlength="255") + input( + type="text" + 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", - placeholder="{{'USER_PROFILE.FIELD.PLACEHOLDER_FULL_NAME' | translate}}", - ng-model="user.full_name", data-required="true", - data-maxlength="256") + 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.LANGUAGE") - select(ng-model="lang", - ng-options="locale.code as locale.name for locale in locales") + 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="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") + 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") + button.button-green.submit-button( + type="submit" + title="{{'COMMON.SAVE' | translate}}", + translate="COMMON.SAVE" + ) + a.delete-account( + href="" + title="{{'USER_PROFILE.ACTION_DELETE_ACCOUNT' | translate}}" + ng-click="ctrl.openDeleteLightbox()" + translate="USER_PROFILE.ACTION_DELETE_ACCOUNT" + ) div.lightbox.lightbox-delete-account(tg-lb-delete-user) diff --git a/app/plugins/.gitignore b/app/plugins/.gitignore new file mode 100644 index 00000000..61d16771 --- /dev/null +++ b/app/plugins/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!main.coffee diff --git a/app/plugins/.hidden b/app/plugins/.hidden deleted file mode 100644 index e69de29b..00000000 diff --git a/app/plugins/humanshtml/templates/humans.html b/app/plugins/humanshtml/templates/humans.html deleted file mode 100644 index 63ee509c..00000000 --- a/app/plugins/humanshtml/templates/humans.html +++ /dev/null @@ -1,10 +0,0 @@ -

Team

- -
    -
  • Andrey Antukh «niwi@niwi.be»
  • -
  • Jesus Espino Garcia «jespinog@gmail.com»
  • -
  • David Barragán Merino «bameda@dbarragan.com»
  • -
  • Xavi Julian «xavier.julian@kaleidos.net»
  • -
  • Alejandro Alonso «alejandro.alonso@kaleidos.net»
  • -
- diff --git a/app/plugins/main.coffee b/app/plugins/main.coffee index 3f7d3e9d..29bd595b 100644 --- a/app/plugins/main.coffee +++ b/app/plugins/main.coffee @@ -1,7 +1,7 @@ ### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino +# Copyright (C) 2014-2015 Andrey Antukh +# Copyright (C) 2014-2015 Jesús Espino Garcia +# Copyright (C) 2014-2015 David Barragán Merino # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -16,7 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -# File: pluggins/main.coffee +# File: modules/backlog.coffee ### module = angular.module("taigaPlugins", ["ngRoute"]) diff --git a/app/plugins/terms/terms.coffee b/app/plugins/terms/terms.coffee deleted file mode 100644 index ef4fa4d2..00000000 --- a/app/plugins/terms/terms.coffee +++ /dev/null @@ -1,53 +0,0 @@ -### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# File: plugins/terms/terms.coffee -### - -taiga = @.taiga - -module = angular.module("taigaPlugins") - -template = _.template(""" -

- By clicking "Sign up", you agree to our
- terms of service - and - privacy policy. -

""") - - -TermsNoticeDirective = ($config) -> - privacyPolicyUrl = $config.get("privacyPolicyUrl") - termsOfServiceUrl = $config.get("termsOfServiceUrl") - - templateFn = -> - if not (privacyPolicyUrl and termsOfServiceUrl) - return "" - - ctx = {termsUrl: termsOfServiceUrl, privacyUrl: privacyPolicyUrl} - return template(ctx) - - return { - scope: {} - restrict: "AE" - template: templateFn - } - - -module.directive("tgTermsNotice", ["$tgConfig", TermsNoticeDirective]) diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index 061f9b68..30817476 100755 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -1,20 +1,20 @@ .button, %button { - @extend %medium; - @extend %title; + @extend %light; + @extend %small; background: transparent; border: 0; color: $white; cursor: pointer; display: inline-block; - padding: .4rem 2.5rem; + padding: .4rem 2rem; text-align: center; text-transform: uppercase; - transition: all .3s linear; + transition: all .2s linear; vertical-align: middle; &:hover { color: $white; - transition: all .3s linear; + transition: all .2s linear; } &:visited { color: $white; @@ -46,9 +46,12 @@ &.active { span, .icon { - color: $green-taiga; + color: $primary; } } + &:visited { + color: $blackish; + } } @@ -59,10 +62,10 @@ .button-green, a.button-green { @extend %button; - background: $green-taiga; + background: $primary; &:hover, &.active { - background: $fresh-taiga; + background: $primary-light; color: $white; } } @@ -73,7 +76,7 @@ a.button-gray { background: $gray; &:hover, &.active { - background: $fresh-taiga; + background: $primary-light; color: $white; } } @@ -111,13 +114,13 @@ a.button-gray { .button-bulk { @extend %button; - background: $green-taiga; + background: $primary; padding: .35rem .5rem; .icon { margin-right: 0; } &:hover { - background: $fresh-taiga; + background: $primary-light; } } diff --git a/app/styles/components/card-placeholder.scss b/app/styles/components/card-placeholder.scss new file mode 100644 index 00000000..27691a88 --- /dev/null +++ b/app/styles/components/card-placeholder.scss @@ -0,0 +1,36 @@ +.card-placeholder { + background: darken($whitish, 2%); + border: 1px dashed darken($whitish, 8%); + cursor: default; + padding: 1rem; + .placeholder-avatar { + display: flex; + } + .image { + background: darken($whitish, 8%); + flex-basis: 48px; + height: 48px; + margin-right: .5rem; + width: 48px; + } + .text { + flex: 1; + } + .line { + background: darken($whitish, 8%); + height: 1rem; + margin-bottom: 1rem; + width: 80%; + &:last-child { + width: 40%; + } + } + .title { + text-transform: uppercase; + } + p { + @extend %light; + color: $gray; + margin: 0; + } +} diff --git a/app/styles/components/check.scss b/app/styles/components/check.scss index 3fbce268..1cada375 100644 --- a/app/styles/components/check.scss +++ b/app/styles/components/check.scss @@ -22,11 +22,11 @@ width: 50%; } ~ .check-text { - //@include transition(opacity .3s linear); @extend %small; + @extend %text; color: $white; position: absolute; - top: .25rem; + top: .2rem; } ~ .check-yes { opacity: 0; @@ -39,16 +39,16 @@ } input:checked { + div { - background-color: $fresh-taiga; + background-color: $primary-light; margin-left: 50%; transition: all .2s linear; } ~ .check-yes { - opacity: .6; - right: .4rem; + opacity: .8; + right: .25rem; } ~ .check-no { - left: .4rem; + left: .25rem; opacity: 0; } } diff --git a/app/styles/components/created-by.scss b/app/styles/components/created-by.scss index bff30e0a..734bd5f9 100644 --- a/app/styles/components/created-by.scss +++ b/app/styles/components/created-by.scss @@ -1,30 +1,27 @@ -.us-created-by { +.ticket-created-by { display: flex; margin-bottom: 1rem; margin-top: .5rem; position: relative; .user-avatar { - flex-basis: 40px; + flex-basis: 3rem; flex-grow: 0; + margin-right: .5rem; img { - border-radius: 8%; width: 100%; } } .created-by { - flex-basis: 70px; - flex-grow: 3; - margin-left: .5rem; .created-title, .created-date { + @extend %light; @extend %small; - color: $gray-light; + color: $gray; display: block; + line-height: 1.5; } - .created-user { - @extend %large; - color: $green-taiga; - cursor: default; + .created-title { + color: $primary; &.editable { cursor: pointer; } diff --git a/app/styles/components/estimation.scss b/app/styles/components/estimation.scss new file mode 100644 index 00000000..3f50666d --- /dev/null +++ b/app/styles/components/estimation.scss @@ -0,0 +1,67 @@ +%estimation { + .points-per-role { + display: flex; + flex-wrap: wrap; + justify-content: center; + } + .ticket-role-points { + background: rgba($gray-light, .1); + border-radius: 2px; + color: rgba($grayer, .3); + flex-basis: 20%; + flex-grow: 1; + flex-shrink: 0; + margin: .1rem; + padding: .5rem 0 .1rem; + position: relative; + text-align: center; + transition: color .3s linear; + &.active { + background: rgba($primary-light, .9); + color: $whitish; + } + &:first-child { + background: rgba($grayer, .25); + border-radius: 0; + color: $whitish; + } + &:last-child { + border: 0; + } + .points { + @extend %larger; + @extend %text; + display: block; + text-align: center; + } + .role { + @extend %small; + @include ellipsis(90%); + display: inline-block; + text-align: center; + } + } + .popover { + @include popover(200px, $top: 105%, $left: 35%, $arrow-width: 10px, $arrow-top: -5px, $arrow-left: 10px, $arrow-height: 10px); + li { + display: inline-block; + width: 23%; + } + a { + display: block; + text-align: center; + &:hover, + &.active { + background: $primary-light; + color: $white; + } + } + &.fix { + @include popover(200px, $top: 105%, $left: -160px, $arrow-width: 10px, $arrow-top: -5px, $arrow-left: 90%, $arrow-height: 10px); + } + } +} + +.ticket-estimation { + @extend %estimation; +} diff --git a/app/styles/components/kanban-task.scss b/app/styles/components/kanban-task.scss index 8f383414..70a6480e 100644 --- a/app/styles/components/kanban-task.scss +++ b/app/styles/components/kanban-task.scss @@ -1,6 +1,6 @@ .kanban-task { - background: $postit; - border: 1px solid $postit-hover; + background: $card; + border: 1px solid $card-hover; box-shadow: none; cursor: move; margin: .2rem; @@ -11,7 +11,7 @@ &:hover { .icon-edit, .icon-drag-h { - color: $postit-dark-hover; + color: $card-dark; display: block; opacity: 1; transition: color .3s linear, opacity .3s linear; @@ -33,13 +33,18 @@ color: $white; } } + &.card-placeholder { + background: darken($whitish, 2%); + border: 3px dashed darken($whitish, 8%); + cursor: default; + } .kanban-tagline { - border-color: $postit-hover; + border-color: $card-hover; display: flex; height: .6rem; } .kanban-tag { - border-top: .3rem solid $postit-hover; + border-top: .3rem solid $card-hover; flex-basis: 0; flex-grow: 1; height: .6rem; @@ -66,7 +71,7 @@ img { margin: 0 auto; &:hover { - border: 2px solid $green-taiga; + border: 2px solid $primary; transition: border .3s linear; } } @@ -77,7 +82,7 @@ padding: 0 .5rem 0 .8rem; } .task-assigned { - color: $postit-dark-hover; + color: $card-dark; display: block; } .task-num { @@ -94,12 +99,12 @@ .icon-edit, .icon-drag-h { @extend %large; - color: $postit-hover; + color: $card-hover; opacity: 0; position: absolute; transition: opacity .2s linear; &:hover { - color: darken($postit-hover, 15%); + color: darken($card-hover, 15%); transition: color .3s linear; } } @@ -149,7 +154,7 @@ } .task-points { @extend %small; - color: darken($postit-hover, 15%); + color: darken($card-hover, 15%); margin: 0; span { display: inline-block; diff --git a/app/styles/components/list-items.scss b/app/styles/components/list-items.scss new file mode 100644 index 00000000..ee3d198f --- /dev/null +++ b/app/styles/components/list-items.scss @@ -0,0 +1,104 @@ +// Like and Watch number component + +// Common styles for all list items +@mixin list-itemtype-common { + @include list-itemtype-track; + border-bottom: 1px solid $whitish; + display: flex; + padding: .5rem; + padding-right: 0; + h2 { + @extend %normal; + @extend %text; + line-height: 1.4; + margin-bottom: 0; + text-transform: none; + } + p { + @extend %light; + margin-bottom: 0; + } + .list-itemtype-avatar { + flex-basis: 3rem; + flex-shrink: 0; + margin-right: .75rem; + min-width: 3rem; + img { + width: 100%; + } + } +} + +.list-itemtype-project { + @include list-itemtype-common; + justify-content: space-between; + h2 { + @extend %large; + } + .list-itemtype-project-members { + align-self: flex-end; + display: flex; + flex-direction: row-reverse; + flex-grow: 0; + flex-wrap: wrap-reverse; + margin-top: 1rem; + img { + border-radius: .1rem; + margin-right: .3rem; + width: 2rem; + } + } + .list-itemtype-project-tags { + align-self: flex-end; + display: flex; + flex: 3; + flex-wrap: wrap; + margin-top: .5rem; + } + .tag { + align-self: flex-end; + margin: 0 .25rem .25rem 0; + padding: .5rem; + } +} + +.list-itemtype-ticket { + @include list-itemtype-common; + h2 { + @extend %medium; + } + .ticket-type, + .ticket-project { + margin-right: .3rem; + } + .ticket-project { + color: $gray-light; + } + .list-itemtype-ticket-data { + flex: 1; + margin-right: 1rem; + } + .ticket-id { + color: $gray-light; + margin-right: .3rem; + } + +} + + +.list-itemtype-user { + @include list-itemtype-common; + h2 { + @extend %large; + } + .extra-info { + @extend %small; + @extend %light; + margin-top: .25rem; + } +} + +.list-itemtype-timeline { + @include list-itemtype-common; + +} diff --git a/app/styles/components/markdown-help.scss b/app/styles/components/markdown-help.scss index 36ad3ea3..f43ae37b 100644 --- a/app/styles/components/markdown-help.scss +++ b/app/styles/components/markdown-help.scss @@ -8,7 +8,7 @@ a.help-button { transition: color .2s linear; } .icon { - color: $fresh-taiga; + color: $primary-light; transition: color .2s linear; } } diff --git a/app/styles/components/notification-message.scss b/app/styles/components/notification-message.scss index f43d9a50..daf23d06 100644 --- a/app/styles/components/notification-message.scss +++ b/app/styles/components/notification-message.scss @@ -1,5 +1,5 @@ .notification-message-success { - background: rgba($fresh-taiga, .95); + background: rgba($primary-light, .95); box-shadow: 0 25px 10px -15px rgba(0, 0, 0, .05); opacity: 1; right: -370px; @@ -96,40 +96,47 @@ } .notification-light { + align-items: center; background: rgba($gray-light, .95); color: $white; - left: calc(50% - 200px); - padding: 1rem 1rem .2rem; + display: flex; + justify-content: space-between; + opacity: 0; + padding: 1rem; position: absolute; top: 0; transform: translateY(-100%); - width: 400px; + transition: all .6s; + width: 100%; z-index: 99999; &.inactive { - transition: all .6s ease-in-out; + opacity: 0; + transform: translateY(-100%); + transition: all .6s; } &.active { + opacity: 1; transform: translateY(0); - transition: all .6s ease-in-out; + transition: all .6s; } - .text { - display: inline-block; - margin-left: .5rem; - width: 80%; + p { + @extend %light; + @extend %small; + margin: 0; } .warning { - @extend %large; - @extend %bold; color: $white; - line-height: 2.4rem; + line-height: 1.5rem; } - .icon-delete { - color: $white; - position: absolute; - right: 1rem; + .close { + margin-right: 1rem; + width: 2rem; + path { + fill: $white; + } } } .notification-message-light-error { - background: rgba($red, .95); + background: rgba($red, .8); } diff --git a/app/styles/components/paginator.scss b/app/styles/components/paginator.scss index 4d8f4ad8..56d0d908 100644 --- a/app/styles/components/paginator.scss +++ b/app/styles/components/paginator.scss @@ -24,7 +24,7 @@ } .active { span { - background: $fresh-taiga; + background: $primary-light; } } .dots { diff --git a/app/styles/components/summary.scss b/app/styles/components/summary.scss index ded4538b..25f26f33 100644 --- a/app/styles/components/summary.scss +++ b/app/styles/components/summary.scss @@ -1,6 +1,8 @@ +$summary-background: $grayer; + .summary { align-content: center; - background: $grayer; + background: $summary-background; color: $white; display: flex; flex-wrap: wrap; @@ -13,12 +15,9 @@ } .data { .number { - color: $fresh-taiga; + color: $primary-light; } } - .icon { - @extend %large; - } .number { @extend %xlarge; @extend %bold; @@ -31,6 +30,47 @@ @extend %light; line-height: 1; } + .stats { + cursor: pointer; + height: 2rem; + margin-left: auto; + width: 2rem; + path { + opacity: 1; + } + &:hover { + .graph, + .color-line { + fill: $primary-light; + transition: fill .2s; + } + } + &.active { + .white-line, + .color-line { + display: none; + } + .graph { + fill: $primary; + } + } + svg { + height: 100%; + width: 100%; + } + .graph, + .color-line { + fill: darken($gray-light, 20%); + transition: fill .2s; + } + .white-line { + fill: $summary-background; + } + .white-line, + .color-line { + display: block; + } + } } .summary-progress-bar { @@ -42,7 +82,7 @@ position: relative; width: 15%; .current-progress { - background: $fresh-taiga; + background: $primary-light; height: 24px; } .defined-points { @@ -57,7 +97,7 @@ position: absolute; } .closed-points-progress { - background: $fresh-taiga; + background: $primary-light; height: 24px; position: absolute; } @@ -92,20 +132,67 @@ font-size: 1.4rem; margin-right: .4rem; &.icon-stats { - color: $gray; + color: $primary; float: right; transition: color .3s linear; &:hover { - color: $fresh-taiga; + color: $primary-light; transition: color .3s linear; } &.active { - color: $fresh-taiga; + color: $primary-light; } &.active:hover { - color: $gray; + color: $primary; transition: color .3s linear; } } } } + +.empty-burndown { + @extend %light; + align-content: center; + align-items: center; + background: rgba($primary-dark, .15); + display: flex; + flex-direction: row; + justify-content: center; + margin-bottom: 1rem; + padding: 2rem 6rem; + svg { + flex-basis: 5rem; + flex-shrink: 0; + margin-right: 1rem; + width: 5rem; + } + p { + margin: 0; + } + .empty-text { + flex: 1; + } + .title { + @extend %light; + @extend %large; + color: $primary-dark; + margin: 0; + text-transform: uppercase; + } + path { + fill: $primary-dark; + } + a { + @extend %bold; + animation: blink 2s infinite; + } +} + +.graphics-container { + $height: 300px; + @include slide($height, hidden, 0); + &.shown { + max-height: $height; + transition: none; + } +} diff --git a/app/styles/components/tag.scss b/app/styles/components/tag.scss index e08792ac..7cf06594 100644 --- a/app/styles/components/tag.scss +++ b/app/styles/components/tag.scss @@ -5,8 +5,8 @@ border-radius: 0 5px 5px 0; color: $grayer; display: inline-block; - margin-right: .5rem; - padding: .5rem .75rem; + margin: 0 .5rem .5rem 0; + padding: .5rem; text-align: center; .icon-delete { color: $gray-light; @@ -22,7 +22,7 @@ border: 1px solid $gray-light; z-index: 99910; .ui-state-focus { - background: $fresh-taiga; + background: $primary-light; } li { cursor: pointer; @@ -49,7 +49,7 @@ .add-tag { color: $gray-light; &:hover { - color: $fresh-taiga; + color: $primary-light; } } .icon-plus { diff --git a/app/styles/components/taskboard-task.scss b/app/styles/components/taskboard-task.scss index 62cc08a0..016d126b 100644 --- a/app/styles/components/taskboard-task.scss +++ b/app/styles/components/taskboard-task.scss @@ -1,6 +1,6 @@ .taskboard-task { - background: $postit; - border: 1px solid $postit-hover; + background: $card; + border: 1px solid $card-hover; box-shadow: none; cursor: move; margin: .2rem; @@ -8,7 +8,7 @@ &:hover { .icon-edit, .icon-drag-h { - color: $postit-dark-hover; + color: $card-dark; display: block; opacity: 1; transition: color .3s linear, opacity .3s linear; @@ -36,13 +36,18 @@ } } } + &.card-placeholder { + background: darken($whitish, 2%); + border: 3px dashed darken($whitish, 8%); + cursor: default; + } .taskboard-tagline { - border-color: $postit-hover; + border-color: $card-hover; display: flex; height: .6rem; } .taskboard-tag { - border-top: .3rem solid $postit-hover; + border-top: .3rem solid $card-hover; flex-basis: 0; flex-grow: 1; height: .6rem; @@ -64,7 +69,7 @@ img { margin: 0 auto; &:hover { - border: 2px solid $green-taiga; + border: 2px solid $primary; transition: border .3s linear; } } @@ -86,10 +91,10 @@ } .task-assigned { @extend %small; - color: $postit-dark-hover; + color: $card-dark; display: block; &:hover { - color: $green-taiga; + color: $primary; } } .task-num { @@ -117,11 +122,11 @@ .icon-drag-h { @extend %large; bottom: .5rem; - color: $postit-hover; + color: $card-hover; opacity: 0; position: absolute; &:hover { - color: $postit-dark-hover; + color: $card-dark; } } .icon-edit, diff --git a/app/styles/components/track-btn.scss b/app/styles/components/track-btn.scss new file mode 100644 index 00000000..ba944611 --- /dev/null +++ b/app/styles/components/track-btn.scss @@ -0,0 +1,184 @@ +////////////////////// +//Watch like buttons +////////////////////// + +.track-buttons-container { + @extend %small; + position: relative; +} + +.track-button { + align-items: stretch; + display: inline-flex; + position: relative; + .track-inner { + align-items: stretch; + background: $whitish; + border-radius: 4px 0 0 4px; + display: flex; + flex: 1; + flex-basis: 140px; + margin-right: .1rem; + min-width: 140px; + &:hover { + background: darken($whitish, 5%); + transition: background .3s; + } + span { + align-self: center; + } + .icon-arrow-up { + margin-left: auto; + padding: 0 .5rem; + transform: rotate(180deg); + } + } + &:hover { + color: $blackish; + } + &.active { + .track-inner { + background: rgba($primary-light, .2); + } + .track-icon { + path { + fill: $primary; + } + } + } + &.watch-container { + margin-right: 1rem; + position: relative; + } + .track-icon { + padding: .3rem .6rem .3rem .75rem; + svg { + height: 1.1rem; + position: relative; + top: 2px; + width: 1.1rem; + } + path { + fill: $grayer; + } + } + .track-button-counter { + align-items: center; + border: 1px solid $whitish; + display: flex; + justify-content: center; + min-width: 2rem; + } +} +.watch-button, +.like-button { + &.active { + &.is-hover { + .track-inner { + background: $red; + color: $whitish; + transition: background .3s; + } + path { + fill: $red-light; + } + } + } +} + +.watch-options { + animation: dropdownFade .2s cubic-bezier(.09, 0, .99, .01) alternate; + background: rgba($black, .9); + border-radius: 4px; + margin: 0; + margin-top: 2.5rem; + min-width: 300px; + padding: 1rem; + position: absolute; + right: 0; + top: 0; + z-index: 99; + &.hidden { + animation: dropdownFade .2s cubic-bezier(.09, 0, .99, .01) reverse; + } + li { + margin: 0; + &:last-child a { + border: 0; + } + } + a { + @extend %light; + border-bottom: 1px solid rgba($gray-light, .3); + color: $white; + display: flex; + justify-content: space-between; + padding: .5rem 0; + &.active { + color: $primary-light; + } + } + .watch-check { + height: 1.25rem; + width: 1.25rem; + path { + fill: $primary-light; + } + } +} + +.upvote-btn { + align-content: center; + align-items: stretch; + display: flex; + flex: 0; + flex-basis: 3rem; + justify-content: center; + margin-right: .3rem; + .vote-inner { + background: $whitish; + color: $gray-light; + display: block; + padding: 1rem; + text-align: center; + } + a { + &:hover { + background: darken($whitish, 5%); + color: $primary-dark; + transition: background .3s; + path { + fill: $primary-dark; + } + } + &.active { + background: rgba($primary-light, .2); + color: $primary; + transition: all .3s; + path { + fill: $primary; + } + } + &.is-hover { + background: $red; + color: $whitish; + svg { + transform: rotate(180deg); + transition: all .3s; + } + path { + fill: $red-light; + } + } + } + svg { + height: 1rem; + width: 1rem; + } + span { + display: block; + } + path { + fill: $gray-light; + } +} diff --git a/app/styles/components/user-list.scss b/app/styles/components/user-list.scss new file mode 100644 index 00000000..0000c4dc --- /dev/null +++ b/app/styles/components/user-list.scss @@ -0,0 +1,84 @@ +%user-list { + .user-list-single { + align-content: center; + align-items: center; + background: transparent; + border-bottom: 1px solid $whitish; + display: flex; + padding: .5rem .5rem .3rem; + vertical-align: middle; + + &:last-child { + border: 0; + } + } + .user-list-avatar { + flex-basis: 3rem; + margin-right: .25rem; + max-width: 3rem; + img { + width: 100%; + } + } + .user-list-name { + @extend %light; + color: $grayer; + margin-left: .5rem; + } +} + +.assigned-to-list { + @extend %user-list; + margin-top: 1rem; + .user-list-single { + &:hover { + background: lighten($primary, 58%); + cursor: pointer; + transition: background .3s linear; + transition-delay: .2s; + } + &.is-active { + background: lighten($primary, 55%); + cursor: pointer; + margin-bottom: 1rem; + position: relative; + transition: background .3s linear; + transition-delay: .1s; + .icon-delete { + opacity: 1; + position: absolute; + right: 1rem; + top: 1.3rem; + transition: opacity .2s ease-in; + } + } + } +} + +.ticket-watchers { + @extend %user-list; + margin-top: 1rem; + .user-list-single { + &:hover { + .icon-delete { + opacity: 1; + transition: opacity .2s ease-in; + } + } + } + .user-list-name { + flex: 1; + position: relative; + } + .icon-delete { + opacity: 0; + position: absolute; + right: .5rem; + top: 0; + transition: all .2s ease-in; + &:hover { + color: $red; + transition: color .3s ease-in; + } + } +} diff --git a/app/styles/components/watchers.scss b/app/styles/components/watchers.scss deleted file mode 100644 index 5c456568..00000000 --- a/app/styles/components/watchers.scss +++ /dev/null @@ -1,66 +0,0 @@ -.watchers { - margin-top: 1rem; - .watchers-header { - border-bottom: 2px solid $gray-light; - padding: .5rem; - position: relative; - .title { - @extend %large; - @extend %title; - text-transform: uppercase; - } - .icon { - @extend %large; - position: absolute; - right: 1rem; - } - &.no-watchers { - border-bottom: 0; - } - } - .watcher-single { - align-content: center; - align-items: center; - background: transparent; - border-bottom: 1px solid $gray-light; - display: flex; - justify-content: center; - padding: .5rem 0 .3rem; - vertical-align: middle; - &:last-child { - border: 0; - } - &:hover { - .icon-delete { - opacity: 1; - transition: opacity .2s ease-in; - } - } - } - .watcher-avatar { - flex-basis: 3rem; - max-width: 3rem; - padding-left: .3rem; - img { - width: 100%; - } - } - .watcher-name { - @extend %small; - color: $grayer; - flex-grow: 8; - margin-left: 1rem; - position: relative; - } - .icon-delete { - opacity: 0; - position: absolute; - right: .5rem; - top: 0; - transition: all .2s ease-in; - &:hover { - color: $red; - transition: color .3s ease-in; - } - } -} diff --git a/app/styles/components/wysiwyg.scss b/app/styles/components/wysiwyg.scss index 18dfa7af..c742fbf3 100644 --- a/app/styles/components/wysiwyg.scss +++ b/app/styles/components/wysiwyg.scss @@ -40,9 +40,9 @@ } } a { - color: $green-taiga; + color: $primary; &:hover { - color: $fresh-taiga; + color: $primary-light; } } p { diff --git a/app/styles/core/animation.scss b/app/styles/core/animation.scss index 1a2fc285..50bc216c 100644 --- a/app/styles/core/animation.scss +++ b/app/styles/core/animation.scss @@ -58,3 +58,12 @@ transform: translateY(0); } } + +@keyframes blink { + 85% { + opacity: 1; + } + 100% { + opacity: .6; + } +} diff --git a/app/styles/core/base.scss b/app/styles/core/base.scss index 607251a4..55e5b543 100644 --- a/app/styles/core/base.scss +++ b/app/styles/core/base.scss @@ -5,8 +5,6 @@ html { } body { @extend %text; - background: $white; // fallback - color: $grayer; line-height: 1.3; min-height: 100%; width: 100%; @@ -17,15 +15,6 @@ body { } &.loading-project { overflow: hidden; - .projects-nav-overlay { - opacity: 1; - overflow: hidden; - transition: opacity 1s ease; - div { - opacity: 1; - transition: opacity 1s ease; - } - } } } @@ -38,14 +27,13 @@ body { } .master { - background: $white; height: 100%; min-height: 100%; position: relative; } .centered { - margin: 0 auto; + margin: 1rem auto; max-width: 1200px; min-width: 960px; width: 90%; @@ -57,7 +45,6 @@ body { } .menu-secondary { - background: $whitish; flex: 0 0 auto; min-height: $main-height; min-width: 0; @@ -84,19 +71,14 @@ body { } .menu-tertiary { - background-color: $dark-taiga; flex: 0 0 auto; min-height: $main-height; width: 250px; } -.extrabar { - background: $whitish; -} - .main { flex: 4; - min-width: 600px; + flex-basis: 600px; padding: 1rem 2rem; } diff --git a/app/styles/core/elements.scss b/app/styles/core/elements.scss index 6644404e..2d50b9f1 100644 --- a/app/styles/core/elements.scss +++ b/app/styles/core/elements.scss @@ -3,16 +3,13 @@ // Blockquotes blockquote, blockquote p { - color: $gray; line-height: 1.25rem; } blockquote { - border-left: 5px solid $whitish; margin: 0 0 20px; padding: .5rem 1.25rem; cite { @extend %small; - color: $gray; display: block; &:before { content: '\2014 \0020'; @@ -59,14 +56,14 @@ sup { } .is-selected { .pika-button { - background: $green-taiga; + background: $primary; border-radius: 0 !important; - box-shadow: inset 0 1px 3px $green-taiga; + box-shadow: inset 0 1px 3px $primary; } } .is-today { .pika-button { - color: $green-taiga; + color: $primary; } &.is-selected { button { @@ -76,32 +73,9 @@ sup { } .pika-button { &:hover { - background: $fresh-taiga !important; //Important added because plugin has it :S + background: $primary-light !important; //Important added because plugin has it :S border-radius: 0 !important; //Important added because plugin has it :S - box-shadow: inset 0 1px 3px $fresh-taiga !important; //Important added because plugin has it :S - } - } -} - - -//Empty -.empty { - border: 1px dashed $gray-light; - color: $gray-light; - min-height: 10rem; - padding: 5% 0; - text-align: center; - .icon { - @extend %xxlarge; - margin-bottom: 2rem; - } - span { - display: block; - &.title { - @extend %xlarge; - @extend %title; - margin-bottom: 1rem; - text-transform: uppercase; + box-shadow: inset 0 1px 3px $primary-light !important; //Important added because plugin has it :S } } } diff --git a/app/styles/core/forms.scss b/app/styles/core/forms.scss index efc8d085..ea30f641 100644 --- a/app/styles/core/forms.scss +++ b/app/styles/core/forms.scss @@ -4,13 +4,7 @@ fieldset { padding: 0; position: relative; width: 100%; - label { - ~.checksley-error-list { - top: 31px; - } - } } - input[type="text"], input[type="number"], input[type="password"], @@ -19,37 +13,28 @@ input[type="date"], input[type="password"], select, textarea { - background: $whitish; - border: 1px solid $gray-light; - color: $grayer; + border: 1px solid; margin: 0; padding: 8px; width: 100%; - @include placeholder { - color: darken($gray-light, 10%); - } &.checksley-error { - border: 1px solid $red; transition: border .3s linear; } } - textarea { min-height: 10rem; resize: vertical; } - - .checksley-error-list { - @extend %small; - background: rgba($whitish, .8); margin-bottom: 0; - padding: 0 .5rem; - position: absolute; - right: 2rem; - top: 10px; li { - color: $red; padding: .2rem; } } +.icon-capslock { + bottom: .6rem; + color: $gray; + display: none; + position: absolute; + right: 1rem; +} diff --git a/app/styles/core/typography.scss b/app/styles/core/typography.scss index 44a8eff3..0f450bb3 100755 --- a/app/styles/core/typography.scss +++ b/app/styles/core/typography.scss @@ -1,14 +1,14 @@ // Typography - // Font face -@each $font-face in OpenSans-CondLight, OpenSans-Light, opensans-regular, opensans-semibold, taiga { +@each $font-face in OpenSans-CondLight, +OpenSans-Light, +opensans-regular, +opensans-semibold, +taiga { @font-face { font-family: '#{$font-face}'; - src: url('../fonts/#{$font-face}.eot?#iefix') format('embedded-opentype'), - url('../fonts/#{$font-face}.woff') format('woff'), - url('../fonts/#{$font-face}.ttf') format('truetype'), - url('../fonts/#{$font-face}.svg#{$font-face}') format('svg'); + src: url('../fonts/#{$font-face}.eot?#iefix') format('embedded-opentype'), url('../fonts/#{$font-face}.woff') format('woff'), url('../fonts/#{$font-face}.ttf') format('truetype'), url('../fonts/#{$font-face}.svg#{$font-face}') format('svg'); } } @@ -21,6 +21,7 @@ h6 { color: $blackish; font-weight: normal; line-height: 1.5; + a { font-weight: inherit; } @@ -32,6 +33,7 @@ h1 { line-height: 1.5; margin-bottom: 1rem; text-transform: uppercase; + span { @extend %xxlarge; margin-right: .5rem; @@ -39,22 +41,27 @@ h1 { text-overflow: ellipsis; vertical-align: bottom; white-space: nowrap; + &.green, &:last-child { flex-shrink: 0; } } + .project-name { display: inline-block; - max-width: 60%; + margin-bottom: 0; } + .project-name-short { display: inline-block; max-width: 40%; } + .green { - color: $green-taiga; + color: $primary; } + .date { @include ellipsis(500px); color: $gray-light; @@ -71,12 +78,16 @@ h2 { p { line-height: 1.5; margin: 0 0 20px; + img { margin: 0; } } -em { font-style: italic; } +em { + font-style: italic; +} + strong { font-weight: bold; } @@ -92,17 +103,16 @@ hr { // __Links__ a, a:visited { - color: $grayer; text-decoration: none; + &:hover { - color: $green-taiga; transition: color .3s linear; } } // Taiga Icons - [data-icon]:before { + // scss-lint:disable ImportantRule content: attr(data-icon); font-family: 'taiga' !important; @@ -118,6 +128,7 @@ a:visited { [class^='icon-']:before, [class*=' icon-']:before { + // scss-lint:disable ImportantRule font-family: 'taiga' !important; -moz-osx-font-smoothing: grayscale; @@ -200,92 +211,86 @@ a:visited { content: 'w'; } .icon-drag-v:before { - content: 'y'; + content: 'x'; } .icon-filter:before { - content: 'z'; + content: 'y'; } .icon-help:before { - content: 'B'; + content: 'z'; } .icon-reload:before { - content: 'C'; + content: 'A'; } .icon-writer:before { - content: 'D'; + content: 'B'; } .icon-stats:before { - content: 'E'; + content: 'C'; } .icon-floppy:before { - content: 'F'; + content: 'D'; } .icon-warning-alt:before { - content: 'H'; + content: 'E'; } .icon-video:before { - content: 'I'; + content: 'F'; } .icon-bulk:before { - content: 'K'; + content: 'G'; } .icon-vunfold:before { - content: 'M'; + content: 'H'; } .icon-tasks:before { - content: 'O'; + content: 'I'; } .icon-kanban:before { - content: 'P'; + content: 'J'; } .icon-search:before { - content: 'Q'; + content: 'K'; } .icon-wiki:before { content: 'L'; } .icon-team:before { - content: 'T'; + content: 'M'; } .icon-vfold:before { content: 'N'; } .icon-issues:before { - content: 'U'; -} -.icon-backlog:before { - content: 'R'; + content: 'O'; } .icon-iocaine:before { - content: 'S'; -} -.icon-closed-eye:before { - content: 'V'; -} -.icon-open-eye:before { - content: 'W'; + content: 'P'; } .icon-archive:before { - content: 'X'; + content: 'Q'; } .icon-capslock:before { - content: 'Y'; + content: 'R'; } .icon-upload:before { - content: 'Z'; + content: 'S'; } .icon-github:before { - content: 'A'; + content: 'T'; } .icon-timeline:before { - content: 'x'; + content: 'U'; +} +.icon-scrum:before { + content: 'V'; } .icon-project:before { - content: 'G'; + content: 'W'; } -.icon-star-fill:before { - content: 'J'; +.icon-heart:before { + content: 'X'; } -.icon-star:before { - content: '0'; +.icon-eye:before { + content: 'Y'; } diff --git a/app/styles/dependencies/colors.scss b/app/styles/dependencies/colors.scss deleted file mode 100755 index e5b28c0d..00000000 --- a/app/styles/dependencies/colors.scss +++ /dev/null @@ -1,37 +0,0 @@ -// Colors - -$black: #000; -$blackish: #050505; -$grayer: #444; -$gray: #555; -$gray-light: #b8b8b8; -$whitish: #f5f5f5; -$white: #fff; - - -$green-taiga: #72a114; -$fresh-taiga: #9dce0a; -$dark-taiga: #879b89; - -$red-light: #ff8282; -$red: #f00; - -$postit: #fff8e4; -$postit-hover: #f1e8cd; -$postit-dark-hover: #cfc29b; - - -//Loading bar -$green-japanese-laurel: #237400; -$green-olive: #618000; -$red-amaranth: #e43050; -$purple-eggplant: #810061; -$yellow-pear: #bbe831; - -$menu: #232323; -$star-fill: #edd400; - -// Top menu values -$top-icon-color: #11241f; -$dropdown-width: 350px; -$dropdown-color: rgba(darken($grayer, 20%), 1); diff --git a/app/styles/dependencies/helpers.scss b/app/styles/dependencies/helpers.scss index 336ce8ce..2cde055c 100644 --- a/app/styles/dependencies/helpers.scss +++ b/app/styles/dependencies/helpers.scss @@ -16,7 +16,6 @@ %mono {font-family: 'courier new', 'monospace';} %lightbox { - background: rgba($white, .95); bottom: 0; display: none; left: 0; @@ -26,10 +25,15 @@ top: 0; z-index: 99910; .close { - @extend %large; + @extend %xlarge; + color: $gray; position: absolute; right: 2rem; - top: 2rem; + top: 1rem; + transition: color .2s; + &:hover { + color: $primary; + } } &.open { align-content: center; @@ -46,9 +50,7 @@ .title { text-align: center; } - input, - textarea, - select { + fieldset { margin-bottom: 1rem; } textarea { @@ -62,17 +64,6 @@ } } -// Background -%triangled-bg { - background: url('/images/bg.png') no-repeat center center; - background-size: cover; -} - -%background-taiga { - background: url('/images/invitation_bg.jpg') no-repeat center center; - background-size: cover; -} - %loading-spinner { animation-timing-function: ease-in-out; animation: rotate 1.5s cubic-bezier(.00, .05, .87, 1.04) infinite alternate; diff --git a/app/styles/dependencies/mixins.scss b/app/styles/dependencies/mixins.scss index 755f4511..acb113d4 100644 --- a/app/styles/dependencies/mixins.scss +++ b/app/styles/dependencies/mixins.scss @@ -57,7 +57,7 @@ border: 0; } &:hover { - color: $fresh-taiga; + color: $primary-light; transition: color .3s linear; &.point { color: $white; @@ -102,3 +102,42 @@ margin-left: calc(-#{$border-size}px + #{$border-width}px); } } + +// Mixin for track buttons +@mixin list-itemtype-track { + .list-itemtype-track { + @extend %small; + color: $gray-light; + display: flex; + flex-basis: 150px; + flex-shrink: 0; + justify-content: flex-end; + .list-itemtype-track-likers { + margin-right: .5rem; + } + } + .list-itemtype-track-likers, + .list-itemtype-track-watchers { + display: flex; + .icon { + display: block; + margin-right: .2rem; + width: 16px; + } + &.active { + color: $primary; + path { + fill: currentcolor; + } + } + } + .icon { + svg { + max-height: 1rem; + max-width: 1rem; + } + path { + fill: currentcolor; + } + } +} diff --git a/app/styles/dependencies/responsive.scss b/app/styles/dependencies/responsive.scss index 94218c7f..e426b4e6 100644 --- a/app/styles/dependencies/responsive.scss +++ b/app/styles/dependencies/responsive.scss @@ -12,8 +12,7 @@ @else if $point == tablet { @media (max-width: 767px) { @content ; } } - @else if $point == mobileonly { + @else if $point == mobile { @media (max-width: 480px) { @content ; } - } } diff --git a/app/styles/extras/dependencies.scss b/app/styles/extras/dependencies.scss index 6d90bfb8..f09f0308 100644 --- a/app/styles/extras/dependencies.scss +++ b/app/styles/extras/dependencies.scss @@ -10,7 +10,7 @@ $prefix-for-mozilla: true; // dependencies //################################################# -@import '../dependencies/colors'; +@import "variables"; @import '../dependencies/mixins'; @import '../dependencies/helpers'; @import '../dependencies/responsive'; diff --git a/app/styles/layout/admin-memberships.scss b/app/styles/layout/admin-memberships.scss index d32e7c43..7d7ad3c4 100644 --- a/app/styles/layout/admin-memberships.scss +++ b/app/styles/layout/admin-memberships.scss @@ -5,4 +5,10 @@ float: right; } } + .check { + input { // IE needs smaller size + height: 40px; + width: 85px; + } + } } diff --git a/app/styles/layout/admin-project-values.scss b/app/styles/layout/admin-project-values.scss index 749d7178..77a20115 100644 --- a/app/styles/layout/admin-project-values.scss +++ b/app/styles/layout/admin-project-values.scss @@ -1,6 +1,16 @@ .admin-attributes { .admin-attributes-section { margin-bottom: 2rem; + .checksley-error-list { + height: 0; + position: relative; + li { + @extend %light; + background: rgba($red, .7); + position: absolute; + width: 100%; + } + } } } @@ -37,8 +47,3 @@ display: inline-block; } } - -//.project-values-options { -// margin-bottom: 1rem; -// text-align: right; -//} diff --git a/app/styles/layout/backlog.scss b/app/styles/layout/backlog.scss index 65941a74..b5edef9e 100644 --- a/app/styles/layout/backlog.scss +++ b/app/styles/layout/backlog.scss @@ -4,17 +4,24 @@ display: flex; justify-content: space-between; margin-bottom: 1rem; - padding: .5rem; .trans-button { + @extend %trans-button; + color: $blackish; display: inline-block; - margin-right: 1rem; - padding: .3rem 0; + padding: .4rem 1.5rem; + &.active, + &:hover { + background: $gray; + color: $whitish; + } + &.active { + &:hover { + background: lighten($gray, 30%); + } + } &.move-to-current-sprint { display: none; } - span { - vertical-align: middle; - } } .button-bulk { margin-left: .2rem; diff --git a/app/styles/layout/invitation.scss b/app/styles/layout/invitation.scss index 6807a2d9..6b51c083 100644 --- a/app/styles/layout/invitation.scss +++ b/app/styles/layout/invitation.scss @@ -11,6 +11,8 @@ align-items: center; display: flex; flex-direction: column; + max-width: 800px; + min-width: 420px; } .avatar { align-items: center; @@ -27,12 +29,12 @@ } } .invitation-text { - @extend %larger; + @extend %large; @extend %light; color: $white; text-align: center; .project-name { - @extend %xxlarge; + @extend %xlarge; display: block; text-transform: uppercase; } @@ -82,9 +84,18 @@ } } } + .register-text { + color: $white; + a { + color: $primary; + } + } + .button-auth { + display: block; + } .button-blackish { &:hover { - background: $green-taiga; + background: $primary; } } .login-form { diff --git a/app/styles/layout/kanban.scss b/app/styles/layout/kanban.scss index 924d451b..b57d9884 100644 --- a/app/styles/layout/kanban.scss +++ b/app/styles/layout/kanban.scss @@ -12,7 +12,4 @@ .burndown-container { display: none; } - .list-filters { - margin-bottom: 1rem; - } } diff --git a/app/styles/layout/login.scss b/app/styles/layout/login.scss index 95c61fff..d911cf3d 100644 --- a/app/styles/layout/login.scss +++ b/app/styles/layout/login.scss @@ -41,17 +41,11 @@ form { margin-bottom: 1rem; } - input { - background: $white; - @include placeholder { - color: $gray-light; - } - } .login-text, .register-text { text-align: center; a:hover { - color: $fresh-taiga; + color: $primary-light; } } .button { @@ -64,10 +58,4 @@ color: $white; } } - .checksley-error-list { - right: 1rem; - +.forgot-pass { - display: none; - } - } } diff --git a/app/styles/layout/not-found.scss b/app/styles/layout/not-found.scss index 3959acf9..a7ced5bb 100644 --- a/app/styles/layout/not-found.scss +++ b/app/styles/layout/not-found.scss @@ -25,9 +25,9 @@ margin-bottom: 3rem; } a { - color: $green-taiga; + color: $primary; &:hover { - color: $fresh-taiga; + color: $primary-light; } } } diff --git a/app/styles/layout/team.scss b/app/styles/layout/team.scss index fba2c6e2..862fbcd3 100644 --- a/app/styles/layout/team.scss +++ b/app/styles/layout/team.scss @@ -3,7 +3,7 @@ margin: 1rem 0; span { &:last-child { - color: $green-taiga; + color: $primary; } } } diff --git a/app/styles/layout/us-detail.scss b/app/styles/layout/ticket-detail.scss similarity index 52% rename from app/styles/layout/us-detail.scss rename to app/styles/layout/ticket-detail.scss index 9a4e8bdf..3fa22c44 100644 --- a/app/styles/layout/us-detail.scss +++ b/app/styles/layout/ticket-detail.scss @@ -1,10 +1,17 @@ .us-story-main-data { margin-bottom: 2rem; + header { + align-content: center; + align-items: stretch; + display: flex; + justify-content: center; + margin-bottom: .5rem; + } .us-title { @extend %large; @extend %text; background: $whitish; - margin-bottom: .5rem; + flex: 1; padding: 1rem; position: relative; transition: all .2s linear; @@ -103,7 +110,7 @@ padding: 0 .2rem; } a:hover { - color: $green-taiga; + color: $primary; } a:first-child { border: 0; @@ -234,222 +241,3 @@ .comment-list { padding: 1rem; } - -.us-detail-status { - @extend %large; - color: $green-taiga; - vertical-align: middle; -} - -.us-detail-progress-bar { - background: $grayer; - height: 26px; - margin-bottom: 1rem; - position: relative; - .current-progress { - background: $fresh-taiga; - height: 26px; - left: 0; - position: absolute; - top: 0; - width: 60%; - } - .tasks-completed { - @extend %small; - color: $white; - left: 10px; - position: absolute; - top: 2px; - } -} - -.points-per-role { - display: flex; - flex-wrap: wrap; - position: relative; - > li { - background: rgba($gray-light, .1); - border-radius: 2px; - color: rgba($grayer, .3); - flex-basis: 80px; - flex-grow: 1; - flex-shrink: 0; - margin: .1rem; - max-width: 50%; - padding: .5rem 0 .1rem; - position: relative; - text-align: center; - transition: color .3s linear; - &.active { - background: rgba($fresh-taiga, .9); - color: $whitish; - } - &:first-child { - background: rgba($grayer, .5); - color: $whitish; - } - &:last-child { - border: 0; - } - } - .points { - @extend %xlarge; - @extend %title; - display: block; - margin-bottom: .3rem; - text-align: center; - } - .role { - @extend %small; - @include ellipsis(90%); - display: inline-block; - text-align: center; - } - .popover { - @include popover(200px, $top: 105%, $left: 35%, $arrow-width: 10px, $arrow-top: -5px, $arrow-left: 10px, $arrow-height: 10px); - li { - display: inline-block; - width: 23%; - } - a { - display: block; - text-align: center; - &:hover, - &.active { - background: $fresh-taiga; - color: $white; - } - } - &.fix { - @include popover(200px, $top: 105%, $left: -160px, $arrow-width: 10px, $arrow-top: -5px, $arrow-left: 90%, $arrow-height: 10px); - } - } - -} - -.duty-data-container { - @extend %small; - margin-bottom: 1rem; - .duty-data { - margin-bottom: .5rem; - &:last-child { - margin: 0; - } - div { - background: darken($whitish, 5%); - padding: .5rem; - padding-right: 1rem; - transition: background .2s ease-in; - } - .clickable { - &:hover { - background: darken($whitish, 10%); - transition: background .2s ease-in; - } - } - } - .level { - display: inline-block; - margin-right: .5rem; - vertical-align: top; - } - .level-name { - color: darken($whitish, 20%); - float: right; - text-transform: lowercase; - } -} - -.us-detail-settings { - margin-top: 2rem; - .button { - color: $white; - display: block; - margin-bottom: .5rem; - text-align: center; - width: 100%; - } - .button-gray { - background: $gray-light; - &:hover { - background: $gray-light; - } - &.editable { - &:hover { - background: $grayer; - cursor: pointer; - } - } - &.active { - background: $green-taiga; - } - } - .item-block { - &.editable { - &:hover { - background: $red; - cursor: pointer; - } - } - } - .button-red { - &:hover, - &.active { - background: $red; - } - } - label { - &.editable { - cursor: pointer; - } - +input { - display: none; - } - } - span { - &.button-gray, - &.button-gray:hover { - background: $gray-light; - &.active { - background: $gray; - } - } - &.button-red, - &.button-red:hover { - background: $red-light; - &.active { - background: $red; - } - } - } - .loading-spinner { - @extend %loading-spinner; - } -} - -.us-status { - .type-data { - position: relative; - .pop-type { - @include popover(150px, '', 30px, '', ''); - } - } - .severity-data { - position: relative; - .pop-severity { - @include popover(150px, '', 30px, '', ''); - } - } - .priority-data { - position: relative; - .pop-priority { - @include popover(150px, '', 30px, '', ''); - } - } - .status-data { - position: relative; - .pop-status { - @include popover(150px, '', 30px, '', ''); - } - } -} diff --git a/app/styles/modules/admin/admin-common.scss b/app/styles/modules/admin/admin-common.scss index 0af84f5e..b247f1e2 100644 --- a/app/styles/modules/admin/admin-common.scss +++ b/app/styles/modules/admin/admin-common.scss @@ -10,10 +10,9 @@ } .total { @extend %large; - @extend %title; background-color: $whitish; color: $grayer; - padding: 1rem; + padding: .3rem 1rem; &:hover { .edit-value { opacity: 1; diff --git a/app/styles/modules/admin/admin-custom-attributes.scss b/app/styles/modules/admin/admin-custom-attributes.scss index a6ba34fa..ee7a21d5 100644 --- a/app/styles/modules/admin/admin-custom-attributes.scss +++ b/app/styles/modules/admin/admin-custom-attributes.scss @@ -24,7 +24,7 @@ } } .row:hover { - background: rgba($fresh-taiga, .05); + background: rgba($primary-light, .05); cursor: move; transition: background .2s linear; .icon-drag-v, @@ -70,9 +70,13 @@ } .custom-description { @include ellipsis(100%); - flex-basis: 90%; + flex-basis: 50%; flex-grow: 8; } + .custom-field-type { + flex-basis: 25%; + flex-grow: 0; + } .custom-options { flex-basis: 100px; flex-grow: 0; @@ -85,7 +89,7 @@ transition: color .2s linear; vertical-align: middle; &:hover { - color: $green-taiga; + color: $primary; transition: color .2s linear; } } diff --git a/app/styles/modules/admin/admin-functionalities.scss b/app/styles/modules/admin/admin-functionalities.scss index 9f0842fe..1a02ff99 100644 --- a/app/styles/modules/admin/admin-functionalities.scss +++ b/app/styles/modules/admin/admin-functionalities.scss @@ -19,7 +19,7 @@ vertical-align: top; width: 32%; &.active { - background-color: rgba($green-taiga, .3); + background-color: rgba($primary, .3); opacity: 1; } .icon { diff --git a/app/styles/modules/admin/admin-membership-table.scss b/app/styles/modules/admin/admin-membership-table.scss index 544dc21f..93b42f1d 100644 --- a/app/styles/modules/admin/admin-membership-table.scss +++ b/app/styles/modules/admin/admin-membership-table.scss @@ -46,20 +46,17 @@ justify-content: space-between; } .active, - .pending { - padding: 8px; + .resend { + padding: .4rem .5rem; } .active { background-color: $whitish; } - .pending { - background-color: $red-light; + .resend { + background-color: $primary; color: $white; - .icon { - float: rsdsdfdvsdvight; - } &:hover { - background-color: $red; + background-color: $primary-light; color: $white; transition: background-color .3s linear; } @@ -71,6 +68,9 @@ color: $red; } } + .pending { + color: $red-light; + } .row-admin { input { vertical-align: middle; diff --git a/app/styles/modules/admin/admin-project-profile.scss b/app/styles/modules/admin/admin-project-profile.scss index cefec654..044cdb76 100644 --- a/app/styles/modules/admin/admin-project-profile.scss +++ b/app/styles/modules/admin/admin-project-profile.scss @@ -44,7 +44,7 @@ } .privacy-project { cursor: pointer; - height: 500px; + height: 50px; left: -10px; opacity: 0; position: absolute; @@ -54,8 +54,8 @@ } .privacy-project:checked { + label { - background: $fresh-taiga; - border: 1px solid $fresh-taiga; + background: $primary-light; + border: 1px solid $primary-light; span { color: $white; } diff --git a/app/styles/modules/admin/admin-roles.scss b/app/styles/modules/admin/admin-roles.scss index 83e0511c..b11656d4 100644 --- a/app/styles/modules/admin/admin-roles.scss +++ b/app/styles/modules/admin/admin-roles.scss @@ -1,7 +1,7 @@ .admin-roles { .role-name { @extend %xlarge; - @extend %title; + @extend %light; color: $grayer; } .edit-value { @@ -13,11 +13,10 @@ transition: opacity .3s linear; } .edit-role { - align-items: left; background-color: $whitish; display: none; margin-bottom: 1rem; - padding: .5rem; + padding: .3rem 1rem; input { background-color: $white; width: 50%; @@ -27,7 +26,7 @@ margin-left: .5rem; transition: color.3s linear; &:hover { - color: $green-taiga; + color: $primary; transition: color.3s linear; } } @@ -39,12 +38,18 @@ padding: .5rem; } .general-category { + @extend %light; + @extend %small; align-items: center; display: flex; justify-content: flex-end; padding-bottom: 2rem; .check { margin-left: .5rem; + input { + height: 40px; + width: 85px; + } } } } diff --git a/app/styles/modules/admin/admin-submenu.scss b/app/styles/modules/admin/admin-submenu.scss index 5a66bf60..6b019c83 100644 --- a/app/styles/modules/admin/admin-submenu.scss +++ b/app/styles/modules/admin/admin-submenu.scss @@ -15,12 +15,12 @@ justify-content: space-between; padding: 1rem 0 1rem 1rem; &:hover { - background: lighten($dark-taiga, 3%); + background: lighten($primary-dark, 3%); color: $white; transition: all .2s; } &.active { - background: lighten($dark-taiga, 10%); + background: lighten($primary-dark, 10%); color: $white; transition: all .2s; } diff --git a/app/styles/modules/admin/admin-third-parties-webhooks.scss b/app/styles/modules/admin/admin-third-parties-webhooks.scss index d3a69af5..8ef4fa0c 100644 --- a/app/styles/modules/admin/admin-third-parties-webhooks.scss +++ b/app/styles/modules/admin/admin-third-parties-webhooks.scss @@ -47,10 +47,10 @@ vertical-align: middle; } a { - color: $green-taiga; + color: $primary; margin-left: .5rem; &:hover { - color: $fresh-taiga; + color: $primary-light; } } } @@ -65,7 +65,7 @@ transition: color .2s linear; vertical-align: middle; &:hover { - color: $green-taiga; + color: $primary; transition: color .2s linear; } @@ -102,7 +102,7 @@ padding: .5rem; transition: background .2s linear; &:hover { - background: rgba($fresh-taiga, .1); + background: rgba($primary-light, .1); transition: background .2s linear; } &.history-single-open { @@ -127,7 +127,7 @@ margin-right: .5rem; width: .8rem; &.history-success { - background: $fresh-taiga; + background: $primary-light; } &.history-error { background: $red; @@ -149,7 +149,7 @@ @extend %small; color: $gray-light; &:hover { - color: $fresh-taiga; + color: $primary-light; transition: color .2s linear; } } diff --git a/app/styles/modules/admin/contrib.scss b/app/styles/modules/admin/contrib.scss index e8745d6b..69744216 100644 --- a/app/styles/modules/admin/contrib.scss +++ b/app/styles/modules/admin/contrib.scss @@ -4,26 +4,31 @@ max-width: 700px; width: 100%; } + h2 { + @extend %text; + @extend %large; + margin-bottom: 0; + } fieldset { + border-bottom: 1px solid $whitish; margin-bottom: 1rem; + padding-bottom: 1rem; } .contrib-input { flex: 1; } .contrib-test { - align-items: flex-end; + align-items: center; display: flex; flex-basis: 7.5rem; + justify-content: center; margin-left: .5rem; text-align: center; - a { - padding: .6rem 2.5rem; - } } label { - @extend %title; + @extend %text; display: block; - margin-bottom: .2rem; + margin-bottom: .25rem; } .contrib-form-wrapper { display: flex; @@ -56,10 +61,10 @@ } .check-item { align-items: center; - border-bottom: 1px solid $whitish; + // border-bottom: 1px solid $whitish; display: flex; justify-content: space-between; - margin-top: 2rem; + margin-top: 1rem; &:last-child { border-bottom: 0; } diff --git a/app/styles/modules/admin/project-values.scss b/app/styles/modules/admin/project-values.scss index e8920a8f..7ef3ae5b 100644 --- a/app/styles/modules/admin/project-values.scss +++ b/app/styles/modules/admin/project-values.scss @@ -33,7 +33,7 @@ } .project-values-row { &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); cursor: move; transition: background .2s ease-in; .icon { @@ -51,12 +51,6 @@ .project-values-value { flex-basis: 0; flex-grow: 1; - .checksley-error-list { - left: 0; - right: auto; - top: 35px; - white-space: nowrap; - } } .project-values-isclosed, @@ -72,7 +66,7 @@ color: $gray; margin-right: .5rem; &:hover { - color: $green-taiga; + color: $primary; transition: color .3s linear; .icon-delete { color: $red; diff --git a/app/styles/modules/auth/login-form.scss b/app/styles/modules/auth/login-form.scss index 1a29f91d..219abdd8 100644 --- a/app/styles/modules/auth/login-form.scss +++ b/app/styles/modules/auth/login-form.scss @@ -2,19 +2,21 @@ .login-password { position: relative; } - input:focus { - +.forgot-pass { - opacity: 0; - transition: opacity .5s linear; + input { + &:focus { + +.forgot-pass { + opacity: 0; + transition: opacity .3s linear; + } } } .forgot-pass { @extend %small; - color: $gray-light; + color: $gray; opacity: 1; position: absolute; right: 1rem; - top: .5rem; + top: .6rem; transition: all .3s linear; &:hover { color: $grayer; diff --git a/app/styles/modules/backlog/backlog-table.scss b/app/styles/modules/backlog/backlog-table.scss index 02d61896..a902d070 100644 --- a/app/styles/modules/backlog/backlog-table.scss +++ b/app/styles/modules/backlog/backlog-table.scss @@ -10,6 +10,7 @@ } .row { @extend %small; + align-items: center; border-bottom: 1px solid $gray-light; display: flex; padding: .5rem 0 .5rem .5rem; @@ -22,6 +23,24 @@ &:hover { background: transparent; } + .input { + flex-basis: 25px; + flex-grow: 0; + flex-shrink: 0; + } + .votes { + color: $gray; + flex-basis: 65px; + flex-grow: 0; + flex-shrink: 0; + text-align: center; + &.inactive { + color: $gray-light; + } + &.is-voted { + color: $primary-light; + } + } .user-stories { overflow: hidden; width: 100%; @@ -67,7 +86,7 @@ @include popover(200px, 0, 65%, '', ''); a { &.active { - background: $fresh-taiga; + background: $primary-light; color: $white; } } @@ -78,6 +97,9 @@ } .pop-points-open { @include popover(200px, 0, 30px, '', ''); + &.pop-bottom { + @include popover(200px, 'auto', 30px, 0, ''); + } li { display: inline-block; width: 23%; @@ -87,7 +109,7 @@ text-align: center; &:hover, &.active { - background: $fresh-taiga; + background: $primary-light; color: $white; } } @@ -96,8 +118,8 @@ .backlog-table-header { .backlog-table-title { + @extend %text; @extend %medium; - @extend %bold; border-bottom: 2px solid $gray-light; flex-wrap: nowrap; padding-right: 30px; @@ -119,8 +141,9 @@ flex-wrap: nowrap; position: relative; &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); transition: background .2s ease-in; + transition-delay: .2s; .us-settings, .icon-drag-v { opacity: 1; @@ -131,7 +154,7 @@ border-bottom: 0; } &.ui-sortable-helper { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); box-shadow: 1px 1px 10px rgba($black, .1); transition: background .2s ease-in; } @@ -147,26 +170,25 @@ height: 40px; width: 100%; } - .row-selected { - background: lighten($green-taiga, 60%); + .row-selected, + .is-checked { + background: lighten($primary, 60%); transition: background .2s ease-in; } + input { + &:checked { + color: $primary-light; + transition: color .2s ease-in; + } + } .user-story-name { align-items: center; display: flex; flex-wrap: nowrap; - input { - flex-shrink: 0; - margin-right: 1rem; - vertical-align: super; - &:checked+a { - color: $fresh-taiga; - transition: color .2s ease-in; - } - } a { + @extend %light; display: inline-block; - flex-grow: 20; + flex: 1; max-width: 90%; overflow: hidden; text-overflow: ellipsis; @@ -245,3 +267,20 @@ padding-right: 45px; } } + +.empty-backlog { + @extend %light; + padding: 2rem; + text-align: center; + img { + margin-bottom: 1rem; + } + .title { + @extend %large; + margin-bottom: .5rem; + text-transform: uppercase; + } + a { + color: $primary; + } +} diff --git a/app/styles/modules/backlog/sprints.scss b/app/styles/modules/backlog/sprints.scss index c96a21b3..c022fa62 100644 --- a/app/styles/modules/backlog/sprints.scss +++ b/app/styles/modules/backlog/sprints.scss @@ -1,17 +1,27 @@ .sprints { - .summary { - background: darken($whitish, 10%); + .sprint-header { + align-content: center; + align-items: center; display: flex; justify-content: space-between; } - .total-sprints { - align-items: flex-start; - color: $grayer; - display: flex; + h1 { + margin: 0; } .add-sprint { - margin: 0; - padding: .3rem 1.5rem; + background: $primary; + padding: .25rem .25rem 0; + transition: background .2s; + &:hover { + background: $primary-light; + } + svg { + height: 1.4rem; + width: 1.5rem; + } + path { + fill: $whitish; + } } .filter-closed-sprints { @extend %small; @@ -69,7 +79,7 @@ transition: all .2s linear; } &:hover { - color: $fresh-taiga; + color: $primary-light; transition: color .2s linear; } } @@ -82,7 +92,7 @@ transition: opacity .2s ease-in; vertical-align: baseline; &:hover { - color: $green-taiga; + color: $primary; transition: color .2s ease-in; } } @@ -126,7 +136,7 @@ position: relative; width: 100%; .current-progress { - background: $green-taiga; + background: $primary; border-radius: 2px; height: 8px; left: 0; @@ -136,6 +146,14 @@ } } .sprint-table { + .sprint-empty { + @extend %light; + background: lighten($gray-light, 18%); + border: 2px dashed lighten($gray-light, 10%); + color: $gray; + padding: 1rem; + text-align: center; + } .row { border-bottom: 1px solid $gray-light; display: flex; @@ -158,7 +176,7 @@ height: 40px; } &.ui-sortable-helper { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); box-shadow: 1px 1px 10px rgba($black, .1); transition: background .2s ease-in; } @@ -170,7 +188,7 @@ padding: 0 4px; } .us-name { - @include ellipsis(250px); + @include ellipsis(230px); display: block; &.closed { color: lighten($gray-light, 5%); @@ -231,3 +249,20 @@ background: $gray-light; } } + +.sprints-empty { + @extend %light; + text-align: center; + img { + margin: 1rem 0; + width: 50%; + } + .title { + @extend %large; + margin-bottom: .5rem; + text-transform: uppercase; + } + a { + color: $primary; + } +} diff --git a/app/styles/modules/backlog/taskboard-table.scss b/app/styles/modules/backlog/taskboard-table.scss index f748be15..6b6b5983 100644 --- a/app/styles/modules/backlog/taskboard-table.scss +++ b/app/styles/modules/backlog/taskboard-table.scss @@ -91,7 +91,7 @@ $column-margin: 0 10px 0 0; margin-right: .3rem; transition: color .2s linear; &:hover { - color: $green-taiga; + color: $primary; } &.hfold, &.hunfold { @@ -178,9 +178,6 @@ $column-margin: 0 10px 0 0; } } } - .taskboard-tasks-box { - background: $whitish; - } .taskboard-userstory-box { padding: .5rem .5rem .5rem 1.5rem; @@ -191,7 +188,7 @@ $column-margin: 0 10px 0 0; top: .7rem; transition: color .2s linear; &:hover { - color: $green-taiga; + color: $primary; } &.icon-plus { right: 2rem; @@ -234,7 +231,7 @@ $column-margin: 0 10px 0 0; .points-list { a { - color: $green-taiga; + color: $primary; margin-left: .5rem; &:hover { span { diff --git a/app/styles/modules/common/assigned-to.scss b/app/styles/modules/common/assigned-to.scss index ee87db88..207ae322 100644 --- a/app/styles/modules/common/assigned-to.scss +++ b/app/styles/modules/common/assigned-to.scss @@ -1,6 +1,7 @@ -.duty-assigned-to { +.ticket-assigned-to { + align-items: center; display: flex; - margin-top: 1rem; + margin-bottom: 1rem; position: relative; &:hover { .assigned-to { @@ -17,30 +18,31 @@ max-width: 2rem; } .user-avatar { - flex-grow: 1; + flex-basis: 4rem; + flex-shrink: 0; img { - border-radius: 8%; width: 100%; } } .assigned-to { - flex-grow: 3; - margin-left: 1rem; + flex-grow: 1; + margin-left: .5rem; .assigned-title { @extend %small; - color: $gray-light; + @extend %light; + color: $gray; display: block; } .user-assigned { @extend %large; - color: $green-taiga; + color: $primary; cursor: default; line-height: 1.5rem; &.editable { cursor: pointer; } .icon { - vertical-align: top; + vertical-align: middle; } } .assigned-name { diff --git a/app/styles/modules/common/attachments.scss b/app/styles/modules/common/attachments.scss index 374170ae..48aa96a0 100644 --- a/app/styles/modules/common/attachments.scss +++ b/app/styles/modules/common/attachments.scss @@ -23,7 +23,7 @@ color: $grayer; cursor: pointer; &:hover { - color: $green-taiga; + color: $primary; transition: color .2s ease-in; } } @@ -45,7 +45,7 @@ } } &.ui-sortable-helper { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); box-shadow: 1px 1px 10px rgba($black, .1); transition: background .2s ease-in; } @@ -96,7 +96,7 @@ display: block; position: absolute; &:hover { - color: $green-taiga; + color: $primary; } } .settings { @@ -143,7 +143,7 @@ } } .percentage { - background: rgba($green-taiga, .1); + background: rgba($primary, .1); bottom: 0; height: 40px; left: 0; @@ -162,11 +162,11 @@ color: $gray-light; } .more-attachments-num { - color: $green-taiga; + color: $primary; margin-left: .5rem; } &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); transition: background .2s ease-in; } } @@ -183,3 +183,8 @@ color: $gray-light; } } + +.attachment-preview img { + max-height: 95vh; + max-width: 95vw; +} diff --git a/app/styles/modules/common/category-config.scss b/app/styles/modules/common/category-config.scss index 7a9346d3..d0a29f25 100644 --- a/app/styles/modules/common/category-config.scss +++ b/app/styles/modules/common/category-config.scss @@ -28,7 +28,7 @@ margin-right: .1rem; width: 15px; &.active { - background: $green-taiga; + background: $primary; } &.inactive { background: $gray; @@ -58,4 +58,10 @@ border-bottom: 0; } } + .check { + input { // IE needs smaller size + height: 40px; + width: 85px; + } + } } diff --git a/app/styles/modules/common/colors-table.scss b/app/styles/modules/common/colors-table.scss index 56caeca1..314c87de 100644 --- a/app/styles/modules/common/colors-table.scss +++ b/app/styles/modules/common/colors-table.scss @@ -24,7 +24,7 @@ justify-content: center; padding: 1rem; &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); cursor: move; transition: background .2s ease-in; .icon { @@ -117,7 +117,7 @@ margin-right: 1rem; opacity: 0; &:hover { - color: $green-taiga; + color: $primary; transition: all .2s ease-in; } &.icon-check-square { @@ -131,7 +131,7 @@ } .icon-check-square { @extend %large; - color: $green-taiga; + color: $primary; } } diff --git a/app/styles/modules/common/custom-fields.scss b/app/styles/modules/common/custom-fields.scss index 9500491a..ed9816b2 100644 --- a/app/styles/modules/common/custom-fields.scss +++ b/app/styles/modules/common/custom-fields.scss @@ -25,6 +25,7 @@ .custom-field-single { border-bottom: 1px solid $whitish; display: flex; + justify-content: flex-start; padding: 1rem; &:hover { .custom-field-options { @@ -43,7 +44,7 @@ color: $gray-light; } a:hover { - color: $green-taiga; + color: $primary; } } } @@ -51,12 +52,12 @@ flex: 0; flex-basis: 200px; .custom-field-name { - @extend %bold; display: block; } .custom-field-description { @extend %small; - color: $gray-light; + @extend %light; + color: lighten($gray, 20%); display: block; line-height: .9rem; } @@ -65,8 +66,13 @@ margin: 0; } .custom-field-value { + @extend %light; + align-items: flex-start; flex: 1; padding: 0 1rem 0 2rem; + &.js-value-view-mode { + white-space: pre-line; + } } form { label { diff --git a/app/styles/modules/common/external-reference.scss b/app/styles/modules/common/external-reference.scss index 7c6c2f43..44336f69 100644 --- a/app/styles/modules/common/external-reference.scss +++ b/app/styles/modules/common/external-reference.scss @@ -20,7 +20,7 @@ padding: 0 .2rem; transition: color .3s linear; &:hover { - color: $green-taiga; + color: $primary; } &:first-child { border: 0; diff --git a/app/styles/modules/common/history.scss b/app/styles/modules/common/history.scss index 688bcb9e..3c6947c3 100644 --- a/app/styles/modules/common/history.scss +++ b/app/styles/modules/common/history.scss @@ -6,7 +6,7 @@ padding: .5rem; &:hover { .icon { - color: $green-taiga; + color: $primary; transform: rotate(180deg); transition: all .2s linear; } @@ -58,7 +58,7 @@ color: $grayer; } &:hover { - color: $green-taiga; + color: $primary; transition: color .2s ease-in; } } @@ -119,7 +119,7 @@ display: block; padding: 1rem 0 1rem 1rem; &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); transition: background .2s ease-in; } } @@ -130,7 +130,7 @@ display: block; padding: 1rem; .prev-comments-num { - color: $green-taiga; + color: $primary; margin-left: .5rem; } } @@ -180,7 +180,7 @@ color: $gray-light; margin-left: .3rem; &:hover { - color: $green-taiga; + color: $primary; transition: color .2s linear; } } @@ -206,12 +206,12 @@ vertical-align: baseline; } &:hover { - color: $green-taiga; + color: $primary; transition: color .2s linear; } } .username { - color: $green-taiga; + color: $primary; margin-bottom: .5rem; } .activity-user { @@ -223,7 +223,7 @@ } } .activity-username { - color: $green-taiga; + color: $primary; margin-bottom: .5rem; } .activity-content { @@ -272,7 +272,7 @@ display: block; padding: 1rem; .prev-activity-num { - color: $green-taiga; + color: $primary; margin-left: .5rem; } } diff --git a/app/styles/modules/common/lightbox.scss b/app/styles/modules/common/lightbox.scss index 198e7cbf..47c34245 100644 --- a/app/styles/modules/common/lightbox.scss +++ b/app/styles/modules/common/lightbox.scss @@ -1,5 +1,9 @@ .lightbox { @extend %lightbox; + h2 { + @extend %larger; + @extend %text; + } } .lightbox-generic-form { @@ -17,9 +21,12 @@ margin-bottom: 1rem; max-height: 9rem; min-height: 7rem; + resize: vertical; } label { + @extend %xsmall; + background: $whitish; border: 1px solid $gray-light; color: $grayer; cursor: pointer; @@ -38,62 +45,55 @@ } .settings { - align-content: flex-start; - align-items: stretch; display: flex; - justify-content: flex-start; + justify-content: center; margin-bottom: 1rem; - fieldset { - flex-basis: 30%; - flex-grow: 1; margin-right: .5rem; - text-align: center; + &:hover { + color: $white; + transition: all .2s ease-in; + } &:last-child { margin: 0; } } - .requirement, .iocaine { &:hover { - background: $fresh-taiga; - border: 1px solid $green-taiga; - color: $white; - transition: all .2s ease-in; + background: $primary-light; + border: 1px solid $primary; } } .blocked { - padding: 8px 30px; &:hover { background: $red-light; border: 1px solid $red; - color: $white; - transition: all .2s ease-in; } } - - .client-requirement, .team-requirement, .iocaine-flag { input:checked+label { - background: $green-taiga; - border: 1px solid $green-taiga; + background: $primary; + border: 1px solid $primary; color: $white; } } .blocking-flag { + label { + align-self: stretch; + display: block; + } input:checked+label { background: $red; border: 1px solid $red; color: $white; } } - input { display: none; } @@ -107,7 +107,6 @@ max-width: 600px; } textarea { - margin-bottom: 1rem; max-height: 12rem; min-height: 15rem; } @@ -119,7 +118,7 @@ flex-grow: 0; max-width: 600px; } - input { + fieldset { margin-bottom: 1rem; } } @@ -143,12 +142,14 @@ margin-left: .5rem; } } + .icon-delete { + @extend %large; + } } .extra-text { margin-top: 1rem; } - input, - select { + fieldset { margin-bottom: 0; } select { @@ -159,6 +160,7 @@ margin-left: .5rem; } .icon-delete { + @extend %xlarge; &:hover { color: $red; } @@ -282,6 +284,9 @@ display: block; } } + .choices { + margin-bottom: 2rem; + } .options { display: flex; a { @@ -312,7 +317,7 @@ @extend %title; } .newsletter { - margin-top: 1rem; + margin: 1rem 0; text-align: center; input { margin-right: .5rem; @@ -400,7 +405,7 @@ padding: 3px; position: relative; .bar { - background: $fresh-taiga; + background: $primary-light; height: 24px; position: absolute; transition: width .1s linear; @@ -465,30 +470,12 @@ flex-grow: 0; width: 600px; } - .watchers { - margin-top: 1rem; - min-height: 440px; - - .watcher-name { - flex-grow: 12; - } - } - .watcher-single { - position: relative; - &:hover, - &.active { - background: lighten($green-taiga, 55%); - cursor: pointer; - } + .user-list-single { &:hover { + background: lighten($primary, 58%); + cursor: pointer; transition: background .3s linear; - } - &.active { - .icon { - opacity: 1; - right: 1rem; - top: 1.3rem; - } + transition-delay: .2s; } } .more-watchers { @@ -500,10 +487,18 @@ } .lb-create-edit-userstory { - .points-per-role { - margin-bottom: 1rem; - li { - margin: .5rem .1rem; + .ticket-role-points { + flex-grow: 1; + flex-shrink: 1; + max-width: calc(100% * (1/5) - .2rem); + &:first-child { + margin-left: 0; + } + &:nth-child(5n+5) { + margin-right: 0; } } + .points-per-role { + margin-bottom: 1rem; + } } diff --git a/app/styles/modules/common/nav.scss b/app/styles/modules/common/nav.scss index b397516c..82d6c1d6 100644 --- a/app/styles/modules/common/nav.scss +++ b/app/styles/modules/common/nav.scss @@ -1,8 +1,6 @@ $label-arrow-wh: 12px; tg-project-menu { - background-color: $dark-taiga; - background-image: url('../images/menu.png'); background-position: 0 -300px; min-height: $main-height; padding: 1rem 0; @@ -22,7 +20,7 @@ tg-project-menu { } a:hover { background: rgba($black, .2); - color: $fresh-taiga; + color: $primary-light; transition: color .3s linear; .helper { @extend %small; @@ -52,7 +50,6 @@ tg-project-menu { } } svg path { - fill: $white; opacity: 1; } span { @@ -70,9 +67,9 @@ tg-project-menu { } .active { background: rgba($black, .2); - color: $fresh-taiga; + color: $primary-light; svg path { - fill: $fresh-taiga; + fill: $primary-light; opacity: 1; } } diff --git a/app/styles/modules/common/projects-nav.scss b/app/styles/modules/common/projects-nav.scss deleted file mode 100644 index ca3d0671..00000000 --- a/app/styles/modules/common/projects-nav.scss +++ /dev/null @@ -1,141 +0,0 @@ -.projects-nav { - background-color: $menu; - display: flex; - flex-direction: column; - height: 100%; - left: 0; - overflow: hidden; - padding: 2rem 1rem; - position: fixed; - top: 0; - transform: translate3d(-300px, 0, 0); - width: 300px; - z-index: 99; - form { - flex-shrink: 0; - } - h1 { - color: $white; - flex-shrink: 0; - margin-bottom: 2rem; - text-align: center; - } - input { - background-color: $gray; - color: $whitish; - } - .icon-search { - position: absolute; - right: .7rem; - top: .7rem; - } - ul { - left: 0; - margin-bottom: 0; - margin-top: 1rem; - position: relative; - top: 0; - width: 100%; - } - .projects-pagination { - display: flex; - flex-direction: column; - margin-top: 1rem; - min-height: 1px; //firefox bug #2057 - } - .create-project-button-wrapper { - display: flex; - flex-shrink: 0; - margin-top: 1rem; - .create-project-button { - flex-grow: 8; - margin-right: .2rem; - text-align: center; - } - .import-project-button { - flex-grow: 1; - padding-left: .5rem; - padding-right: .5rem; - text-align: center; - .icon { - color: $grayer; - margin: 0; - } - } - } - .v-pagination-previous, - .v-pagination-next { - flex-shrink: 0; - } -} - -.projects-list { - a { - @extend %large; - @extend %title; - color: $whitish; - display: block; - padding: 1rem; - position: relative; - text-transform: uppercase; - width: 100%; - &.active, - &:hover { - background-color: $gray; - color: $green-taiga; - transition: background-color .3s linear; - .icon { - opacity: 1; - transition: opacity .3s linear; - } - } - .project-name { - display: block; - max-width: 90%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - .icon { - color: $whitish; - opacity: 0; - position: absolute; - right: 1rem; - top: 1rem; - } - li { - border-bottom: 2px solid $gray; - } -} - -.projects-nav-overlay { - // @include transition (all 1s ease); - @extend %background-taiga; - // background-blend-mode: multiply; - // background-color: $green-taiga; - bottom: 0; - display: none; - left: 0; - opacity: 0; - position: fixed; - right: 0; - top: 0; - width: 100%; - z-index: 99900; - .container { - left: -200px; - margin: 0 auto; - margin-top: 15%; - opacity: 0; - position: relative; - transform: translateY(-50%); - width: 150px; - } - p { - @extend %medium; - color: $fresh-taiga; - padding-top: 20px; - text-align: center; - } -} diff --git a/app/styles/modules/common/related-tasks.scss b/app/styles/modules/common/related-tasks.scss index e40ff5b8..9655cee8 100644 --- a/app/styles/modules/common/related-tasks.scss +++ b/app/styles/modules/common/related-tasks.scss @@ -19,7 +19,7 @@ color: $grayer; cursor: pointer; &:hover { - color: $green-taiga; + color: $primary; transition: color .2s ease-in; } } @@ -153,7 +153,7 @@ } .iocaine, .iocaine:hover { - background: rgba($fresh-taiga, .3); + background: rgba($primary-light, .3); .icon-iocaine { @extend %large; display: inline-block; diff --git a/app/styles/modules/common/ticket-data.scss b/app/styles/modules/common/ticket-data.scss new file mode 100644 index 00000000..a727f403 --- /dev/null +++ b/app/styles/modules/common/ticket-data.scss @@ -0,0 +1,200 @@ +.ticket-data { + .ticket-title { + @extend %larger; + @extend %light; + margin: 1.5rem 0 2rem; + text-transform: uppercase; + span { + margin-right: .25rem; + &:last-child { + @extend %large; + } + } + } + .ticket-data-container { + @extend %small; + @extend %normal; + margin-bottom: 1rem; + .icon { + margin-left: .25rem; + } + } + .level { + display: inline-block; + margin-right: .5rem; + vertical-align: top; + } + .level-name { + color: darken($whitish, 20%); + float: right; + text-transform: lowercase; + } + + .ticket-estimation { + .ticket-role-points { + max-width: 25%; + &:first-child { + flex-basis: 100%; + max-width: 100%; + } + } + } +} + +.ticket-status { + margin-bottom: .5rem; + &:last-child { + margin: 0; + } + div { + background: darken($whitish, 5%); + padding: .5rem; + padding-right: 1rem; + transition: background .2s ease-in; + } + .clickable { + &:hover { + background: darken($whitish, 10%); + transition: background .2s ease-in; + } + } + .type-data { + position: relative; + .pop-type { + @include popover(150px, '', 30px, '', ''); + } + } + .severity-data { + position: relative; + .pop-severity { + @include popover(150px, '', 30px, '', ''); + } + } + .priority-data { + position: relative; + .pop-priority { + @include popover(150px, '', 30px, '', ''); + } + } + .status-data { + position: relative; + .pop-status { + @include popover(150px, '', 30px, '', ''); + } + } +} + +.ticket-track-buttons { + .track-inner { + @extend %light; + @extend %small; + background: darken($whitish, 5%); + padding: .25rem; + text-transform: uppercase; + transition: background .25s; + &:hover { + background: darken($whitish, 10%); + } + } + .track-button { + width: 100%; + } + .active { + .track-button-counter { + background: rgba($grayer, .5); + } + &:hover { + .track-inner { + background: rgba($primary-light, .2); + } + } + &.is-hover { + .track-inner { + background: $red; + color: $whitish; + transition: background .3s; + } + path { + fill: $red-light; + } + } + } + .track-button-counter { + @extend %large; + background: rgba($grayer, .25); + color: $whitish; + padding: 0 .5rem; + } + .vote-button { + margin-bottom: .3rem; + } + .watch-button { + border-bottom: 0; + } + .ticket-watchers { + margin: .5rem 0; + } + .add-watcher { + display: block; + margin: .5rem; + .icon { + background: rgba($grayer, .25); + color: $whitish; + margin-right: .5rem; + padding: .25rem; + } + &:hover { + .icon { + background: $primary-light; + color: $whitish; + transition: background .3s linear; + } + } + } +} + +.ticket-detail-settings { + margin-top: 2rem; + label, + .button { + display: block; + margin-bottom: .5rem; + text-align: center; + &.editable { + cursor: pointer; + } + +input { + display: none; + } + } + .loading-spinner { + @extend %loading-spinner; + } + .button-gray { + background: $gray-light; + &:hover { + background: $gray-light; + } + &.editable { + &:hover { + background: $grayer; + cursor: pointer; + } + } + &.active { + background: $primary; + } + } + .item-block { + &.editable { + &:hover { + background: $red; + cursor: pointer; + } + } + } + .button-red { + display: block; + margin-top: 2rem; + } +} diff --git a/app/styles/modules/common/wizard.scss b/app/styles/modules/common/wizard.scss index b06d7a29..75c60ac7 100644 --- a/app/styles/modules/common/wizard.scss +++ b/app/styles/modules/common/wizard.scss @@ -1,6 +1,6 @@ .wizard-create-project { @extend %lightbox; - background: url('/images/invitation_bg.jpg') no-repeat center center; + @extend %background-taiga; background-size: cover; color: $white; text-align: center; @@ -77,7 +77,7 @@ } input:checked { +label { - background: rgba($fresh-taiga, .7); + background: rgba($primary-light, .7); transition: background .3s ease-in; } } @@ -90,7 +90,7 @@ text-align: center; transition: background .3s ease-in; &:hover { - background: rgba($green-taiga, .7); + background: rgba($primary, .7); transition: background .3s ease-in; } .icon { @@ -160,12 +160,8 @@ left: 25%; } &:nth-child(2) { - // left: 50%; left: 75%; } - // &:nth-child(3) { - // left: 75%; - // } } } .progress-bar-wrapper { @@ -173,7 +169,7 @@ height: .5rem; } .bar { - background: rgba($fresh-taiga, .9); + background: rgba($primary-light, .9); height: .5rem; left: 0; position: absolute; diff --git a/app/styles/modules/create-project.scss b/app/styles/modules/create-project.scss index f7958a33..dc22aefd 100644 --- a/app/styles/modules/create-project.scss +++ b/app/styles/modules/create-project.scss @@ -47,7 +47,7 @@ margin-bottom: .5rem; text-align: center; &:hover { - background: $fresh-taiga; + background: $primary-light; } } a { diff --git a/app/styles/modules/filters/filters.scss b/app/styles/modules/filters/filters.scss index 5a5444a7..527f44cc 100644 --- a/app/styles/modules/filters/filters.scss +++ b/app/styles/modules/filters/filters.scss @@ -75,7 +75,7 @@ } .custom-filters { .title { - color: $green-taiga; + color: $primary; } } a { @@ -84,7 +84,7 @@ padding: .5rem 0 .5rem .5rem; transition: color .2s ease-in; &:hover { - color: $green-taiga; + color: $primary; transition: color .2s ease-in; .icon { opacity: 1; diff --git a/app/styles/modules/help/joyride.scss b/app/styles/modules/help/joyride.scss new file mode 100644 index 00000000..c9938f01 --- /dev/null +++ b/app/styles/modules/help/joyride.scss @@ -0,0 +1,58 @@ +// scss-lint:disable SelectorFormat, QualifyingElement + +.introjs-overlay { + background: radial-gradient(center, ellipse cover, rgba($white, .2) 0, rgba($whitish, .2) 100%); + background-color: $whitish; +} +.introjs-helperLayer { + border: 1px solid rgba($primary-light, .8); +} + +.introjs-helperLayer, +.introjs-tooltip { + box-shadow: 0 1px 8px rgba($grayer, .2); +} + +.introjs-tooltip { + h3 { + @extend %large; + margin-bottom: .5rem; + } + p { + @extend %light; + line-height: 1.4; + margin-bottom: 0; + } +} + +.introjs-bullets { + ul { + li { + a.active { + background: $primary-light; + } + } + } +} + +.introjs-button { + background-color: $primary; + background-image: none; + border: 0; + border-radius: 0; + color: $white; + margin-top: 10px; + padding: .3rem .8rem; + text-shadow: none; + &:focus, + &:hover { + background: none; + background-color: $primary-light; + color: $white; + } + &.introjs-disabled { + background: $whitish; + background-color: none; + color: $white; + } +} diff --git a/app/styles/modules/home-project.scss b/app/styles/modules/home-project.scss index 3300ee9d..187d441c 100644 --- a/app/styles/modules/home-project.scss +++ b/app/styles/modules/home-project.scss @@ -2,15 +2,35 @@ .single-project-intro { margin-bottom: 2rem; } + .intro-options { + align-items: center; + display: flex; + justify-content: space-between; + } h1 { + color: $primary; + display: inline-block; line-height: 1.2; - margin: 0; + margin-bottom: 0; + margin-right: 3rem; vertical-align: middle; } .private { font-size: 1rem; vertical-align: super; } + .like-watch-container { + margin-left: auto; + } + .track-buttons-container { + display: flex; + } + .like-button { + margin-right: .75rem; + } + .track-container { + @include list-itemtype-track; + } .description { @extend %light; @extend %medium; @@ -37,7 +57,7 @@ flex: 1; flex-shrink: 3; margin-right: 1rem; - max-width: 800px; + max-width: 960px; width: 0; } .involved-data { @@ -49,54 +69,19 @@ display: flex; flex-wrap: wrap; margin-bottom: 1rem; - a { - display: block; + li { margin-right: .14rem; width: 24%; &:nth-child(4n) { margin-right: 0; } } + a { + display: block; + } img { border-radius: .2rem; width: 100%; } } } - - -//.summary-stats { -// align-items: flex-start; -// display: flex; -// .info-num { -// @extend %xlarge; -// @extend %bold; -// float: left; -// margin-right: .3rem; -// position: relative; -// top: 5px; -// } -// .info-text { -// @extend %small; -// float: left; -// line-height: .9rem; -// } -//} -// -//.project-data-container { -// display: flex; -// justify-content: space-between; -// ul { -// flex-grow: 0; -// max-width: 33%; -// } -// li { -// display: inline-block; -// margin-right: .1rem; -// width: 10%; -// figure { -// width: 100%; -// } -// } -//} -// diff --git a/app/styles/modules/filters/list-filters.scss b/app/styles/modules/issues/issues-options.scss similarity index 82% rename from app/styles/modules/filters/list-filters.scss rename to app/styles/modules/issues/issues-options.scss index 56e0a7e9..3e00b74f 100644 --- a/app/styles/modules/filters/list-filters.scss +++ b/app/styles/modules/issues/issues-options.scss @@ -1,10 +1,9 @@ -.list-filters { +.issues-options { align-items: center; background-color: $whitish; display: flex; justify-content: flex-end; margin-bottom: 2rem; - padding: .5rem 1rem; .button-bulk { margin-left: .2rem; } diff --git a/app/styles/modules/issues/issues-table.scss b/app/styles/modules/issues/issues-table.scss index 22a4a2ec..3b4f8a26 100644 --- a/app/styles/modules/issues/issues-table.scss +++ b/app/styles/modules/issues/issues-table.scss @@ -6,15 +6,22 @@ } .row { &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); transition: background .2s ease-in; } .icon { display: inline; } + &.is-blocked { + background: lighten($red-light, 20%); + .blocked-text { + color: $red; + margin-right: .5rem; + } + } } .row-selected { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); transition: background .2s ease-in; } .title { @@ -27,6 +34,9 @@ div { cursor: pointer; } + .votes { + color: $gray; + } } .table-main { @extend %small; @@ -51,6 +61,19 @@ text-align: center; width: 75px; } + .votes { + color: $gray; + flex-basis: 75px; + flex-shrink: 0; + text-align: center; + width: 75px; + &.inactive { + color: $gray-light; + } + &.is-voted { + color: $primary-light; + } + } .subject { overflow: hidden; padding-right: 1rem; @@ -126,4 +149,18 @@ } } - +.empty-issues { + margin-top: 4rem; + text-align: center; + img { + margin-bottom: 1rem; + } + .title { + @extend %large; + text-transform: uppercase; + } + p { + @extend %light; + margin: 0; + } +} diff --git a/app/styles/modules/kanban/kanban-table.scss b/app/styles/modules/kanban/kanban-table.scss index d4c0485e..2b7737d3 100644 --- a/app/styles/modules/kanban/kanban-table.scss +++ b/app/styles/modules/kanban/kanban-table.scss @@ -86,9 +86,6 @@ $column-margin: 0 10px 0 0; color: $gray-light; margin-right: .3rem; transition: color .2s linear; - &:hover { - color: $green-taiga; - } &.hfold, &.hunfold { display: inline-block; @@ -96,6 +93,39 @@ $column-margin: 0 10px 0 0; } } } + .icon-open-eye, + .icon-closed-eye { + display: inline-block; + svg { + fill: $gray-light; + height: 1.1rem; + margin-top: .25rem; + width: 1.1rem; + + } + &:hover { + svg { + fill: $primary; + + } + } + } + .icon-open-eye { + .svg-eye-closed { + display: none; + } + .svg-eye-open { + display: block; + } + } + .icon-closed-eye { + .svg-eye-closed { + display: block; + } + .svg-eye-open { + display: none; + } + } } .kanban-table-body { diff --git a/app/styles/modules/search/search-in.scss b/app/styles/modules/search/search-in.scss index d66fdcf3..19eecf56 100644 --- a/app/styles/modules/search/search-in.scss +++ b/app/styles/modules/search/search-in.scss @@ -6,10 +6,15 @@ color: $gray-light; } } - .icon-search { - color: $gray-light; + .icon-search-wrapper { position: absolute; right: .7rem; top: .7rem; } + .icon-search { + color: $gray-light; + } + .loading-spinner { + margin-top: .1rem; + } } diff --git a/app/styles/modules/search/search-result-table.scss b/app/styles/modules/search/search-result-table.scss index f01f2822..65371e7a 100644 --- a/app/styles/modules/search/search-result-table.scss +++ b/app/styles/modules/search/search-result-table.scss @@ -1,11 +1,4 @@ .search-result-table { - - .empty { - .title { - border: 0; - } - } - .row { align-content: center; align-items: center; @@ -13,7 +6,7 @@ justify-content: space-between; padding: .5rem; &:hover { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); transition: background .2s ease-in; } .ref { @@ -40,7 +33,7 @@ } } .row-selected { - background: lighten($green-taiga, 60%); + background: lighten($primary, 60%); transition: background .2s ease-in; } @@ -65,14 +58,6 @@ } } } - .title { - @extend %medium; - @extend %bold; - border-bottom: 1px solid $gray-light; - &:hover { - background: transparent; - } - } .table-main { @extend %small; border-bottom: 1px solid $whitish; @@ -97,3 +82,19 @@ } } } + +.empty-search-results { + margin-top: 4rem; + text-align: center; + img { + margin-bottom: 1rem; + } + .title { + @extend %large; + text-transform: uppercase; + } + p { + @extend %light; + margin: 0; + } +} diff --git a/app/styles/modules/team/team-filters.scss b/app/styles/modules/team/team-filters.scss index 1d39765d..e638fe0d 100644 --- a/app/styles/modules/team/team-filters.scss +++ b/app/styles/modules/team/team-filters.scss @@ -18,7 +18,7 @@ padding: 1rem 0 1rem 1rem; &:hover, &.active { - color: $green-taiga; + color: $primary; transition: color .3s linear; } &.active.icon { diff --git a/app/styles/modules/team/team-table.scss b/app/styles/modules/team/team-table.scss index b6ccef7f..a75332d4 100644 --- a/app/styles/modules/team/team-table.scss +++ b/app/styles/modules/team/team-table.scss @@ -28,7 +28,7 @@ color: $grayer; } .top { - color: $fresh-taiga; + color: $primary-light; opacity: 1; } &:hover { diff --git a/app/styles/modules/user-settings/mail-notifications-table.scss b/app/styles/modules/user-settings/mail-notifications-table.scss index 01fffa42..adc7da4b 100644 --- a/app/styles/modules/user-settings/mail-notifications-table.scss +++ b/app/styles/modules/user-settings/mail-notifications-table.scss @@ -33,7 +33,7 @@ display: none; &:checked { +label { - background: $green-taiga; + background: $primary; transition: background .3s linear; } } @@ -45,7 +45,7 @@ display: block; padding: .5rem; &:hover { - background: $fresh-taiga; + background: $primary-light; cursor: pointer; transition: background .3s linear; } diff --git a/app/styles/modules/wiki/wiki-summary.scss b/app/styles/modules/wiki/wiki-summary.scss index 0328f1eb..cadf915f 100644 --- a/app/styles/modules/wiki/wiki-summary.scss +++ b/app/styles/modules/wiki/wiki-summary.scss @@ -22,7 +22,7 @@ } .username { @extend %large; - color: $fresh-taiga; + color: $primary-light; white-space: nowrap; } } diff --git a/app/styles/shame/shame.scss b/app/styles/shame/shame.scss index 43772411..9342e459 100644 --- a/app/styles/shame/shame.scss +++ b/app/styles/shame/shame.scss @@ -1,8 +1,8 @@ -// Shame SCSS decalrations to be refactorized +// Shame SCSS declarations to be refactorized _:-ms-fullscreen, :root .taskboard-table-body { .task-row { min-height: auto; } -} \ No newline at end of file +} diff --git a/app/styles/vendor/jquery.textcomplete.css b/app/styles/vendor/jquery.textcomplete.css index ce74c742..978cc324 100644 --- a/app/styles/vendor/jquery.textcomplete.css +++ b/app/styles/vendor/jquery.textcomplete.css @@ -17,10 +17,12 @@ .dropdown-menu li:hover, .dropdown-menu .active { background-color: rgb(110, 183, 219); + color: white; } -.textcomplete-wrapper { - width: 100%; +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: white; } /* SHOULD not modify */ diff --git a/app/svg/add.svg b/app/svg/add.svg new file mode 100644 index 00000000..77a2901a --- /dev/null +++ b/app/svg/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/svg/check.svg b/app/svg/check.svg new file mode 100644 index 00000000..d97a17c3 --- /dev/null +++ b/app/svg/check.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app/svg/eye.svg b/app/svg/eye.svg new file mode 100644 index 00000000..98489d10 --- /dev/null +++ b/app/svg/eye.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/app/svg/graph.svg b/app/svg/graph.svg new file mode 100644 index 00000000..b4a28d94 --- /dev/null +++ b/app/svg/graph.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/svg/like.svg b/app/svg/like.svg new file mode 100644 index 00000000..dd7c202d --- /dev/null +++ b/app/svg/like.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/svg/logo-color.svg b/app/svg/logo-color.svg new file mode 100644 index 00000000..3ba218cd --- /dev/null +++ b/app/svg/logo-color.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/svg/logo-nav.svg b/app/svg/logo-nav.svg index f18d9ef6..ff0d29ec 100644 --- a/app/svg/logo-nav.svg +++ b/app/svg/logo-nav.svg @@ -1,18 +1,18 @@ - + \ No newline at end of file + diff --git a/app/svg/logo.svg b/app/svg/logo.svg index 346d8115..ef01257e 100644 --- a/app/svg/logo.svg +++ b/app/svg/logo.svg @@ -1,6 +1,6 @@ - +
') + .css({ + position: 'fixed', + height: size + 'px', + width: size + 'px', + 'background-color': color, + top: ev.clientY, + left: ev.clientX, + + 'z-index': 9999, + + // make sure this dot won't interfere with the mouse events of other elements + 'pointer-events': 'none' + }) + .appendTo('body'); + + setTimeout(function() { + dotEl.remove(); + }, 1000) + + } + + $document.on({ + click: addDot, + dblclick: addDot, + mousemove: addDot + }); + + }); + }; + browser.addMockModule('trackMouse', trackMouse); + + require('./e2e/capabilities.js'); + + browser.driver.manage().window().maximize(); + + browser.getCapabilities().then(function (cap) { + browser.browserName = cap.caps_.browserName; + }); + + browser.get(browser.params.glob.host + 'login'); + + var username = $('input[name="username"]'); + username.sendKeys('admin'); + + var password = $('input[name="password"]'); + password.sendKeys('123123'); + + $('.submit-button').click(); + + return browser.driver.wait(function() { + return utils.common.closeCookies() + .then(function() { + return browser.driver.getCurrentUrl(); + }) + .then(function(url) { + return url === browser.params.glob.host; + }); + }, 10000) + .then(function() { + return utils.common.closeJoyride(); + }) + .then(function() { + return browser.getCapabilities(); + }).then(function (cap) { + browser.browserName = cap.caps_.browserName; + }) + .then(function() { + return browser.get(browser.params.glob.host); + }); + } +} diff --git a/conf/conf.example.json b/conf/conf.example.json index 2a78a5ea..d039bb77 100644 --- a/conf/conf.example.json +++ b/conf/conf.example.json @@ -4,6 +4,8 @@ "debug": true, "debugInfo": false, "defaultLanguage": "en", + "themes": ["taiga"], + "defaultTheme": "taiga", "publicRegisterEnabled": true, "feedbackEnabled": true, "privacyPolicyUrl": null, diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..04372833 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,32 @@ +# Taiga e2e tests # + +### Setup ### + +``` +npm install +npm install -g protractor +npm install -g mocha +npm install -g babel + +webdriver-manager update +``` + +### Usage ### + +After taiga-back and taiga-front are running + +``` +webdriver-manager start +``` + +for auth test: + +``` +protractor conf.e2e.js --suite=auth +``` + +For full tests + +``` +protractor conf.e2e.js --suite=full +``` diff --git a/e2e/auth/auth.e2e.js b/e2e/auth/auth.e2e.js new file mode 100644 index 00000000..6a77adad --- /dev/null +++ b/e2e/auth/auth.e2e.js @@ -0,0 +1,183 @@ +var utils = require('../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('auth', function() { + it('login', async function() { + browser.get(browser.params.glob.host + 'login'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot("auth", "login"); + + var username = $('input[name="username"]'); + username.sendKeys('admin'); + + var password = $('input[name="password"]'); + password.sendKeys('123123'); + + $('.submit-button').click(); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host); + }); + + describe('page without perms', function() { + let path = 'project/project-4/'; + + before(function() { + return utils.common.topMenuOption(6); + }); + + it("redirect to login", async function() { + browser.get(browser.params.glob.host + path); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host + 'login?next=' + encodeURIComponent('/' + path)); + }); + + it("login redirect to the previous one", async function() { + $('input[name="username"]').sendKeys('admin'); + $('input[name="password"]').sendKeys('123123'); + $('.submit-button').click(); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host + path); + }); + }); + + describe("user", function() { + var user = {}; + + before(function() { + utils.common.login('admin', '123123'); + }); + + it("logout", async function() { + await utils.common.login('admin', '123123'); + + browser.actions().mouseMove($('div[tg-dropdown-user]')).perform(); + $$('.dropdown-user li a').last().click(); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host + 'login'); + }); + + describe("register", function() { + it('screenshot', async function() { + browser.get(browser.params.glob.host + 'register'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot("auth", "register"); + }); + + it('register validation', function() { + browser.get(browser.params.glob.host + 'register'); + + $('.submit-button').click(); + + utils.common.takeScreenshot("auth", "register-validation"); + + expect($$('.checksley-required').count()).to.be.eventually.equal(4); + }); + + it('register ok', function() { + browser.get(browser.params.glob.host + 'register'); + + user.username = "username-" + Math.random(); + user.fullname = "fullname-" + Math.random(); + user.password = "passsword-" + Math.random(); + user.email = "email-" + Math.random() + "@taiga.io"; + + $('input[name="username"]').sendKeys(user.username); + $('input[name="full_name"]').sendKeys(user.fullname); + $('input[name="email"]').sendKeys(user.email); + $('input[name="password"]').sendKeys(user.password); + + $('.submit-button').click(); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host); + }); + }); + + describe("change password", function() { + beforeEach(async function() { + await utils.common.login(user.username, user.password); + + browser.get(browser.params.glob.host + 'user-settings/user-change-password'); + }); + + it("error", function() { + $('#current-password').sendKeys('wrong'); + $('#new-password').sendKeys('123123'); + $('#retype-password').sendKeys('123123'); + + $('.submit-button').click(); + + expect(utils.notifications.error.open()).to.be.eventually.equal(true); + }); + + it("success", function() { + $('#current-password').sendKeys(user.password); + $('#new-password').sendKeys(user.password); + $('#retype-password').sendKeys(user.password); + + $('.submit-button').click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + }); + }); + + describe("remember password", function() { + beforeEach(function() { + browser.get(browser.params.glob.host + 'forgot-password'); + }); + + it ("screenshot", async function() { + await utils.common.waitLoader(); + + utils.common.takeScreenshot("auth", "remember-password"); + }); + + it ("error", function() { + $('input[name="username"]').sendKeys("xxxxxxxx"); + $('.submit-button').click(); + + expect(utils.notifications.errorLight.open()).to.be.eventually.equal(true); + }); + + it ("success", async function() { + $('input[name="username"]').sendKeys(user.username); + $('.submit-button').click(); + + await utils.lightbox.open('.lightbox-generic-success'); + + utils.common.takeScreenshot('auth', 'remember-password-success'); + + $('.lightbox-generic-success .button-green').click(); + + await utils.lightbox.close('.lightbox-generic-success'); + }); + }); + + describe("accout", function() { + before(function() { + utils.common.login(user.username, user.password); + }); + + it("delete", async function() { + browser.get(browser.params.glob.host + 'user-settings/user-profile'); + $('.delete-account').click(); + + await utils.lightbox.open('.lightbox-delete-account'); + + utils.common.takeScreenshot("auth", "delete-account"); + + $('.lightbox-delete-account .button-green').click(); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host + 'login'); + }); + }); + }); +}); diff --git a/e2e/capabilities.js b/e2e/capabilities.js new file mode 100644 index 00000000..f754e79f --- /dev/null +++ b/e2e/capabilities.js @@ -0,0 +1,102 @@ +//////////////////////////////////////////////// +// Protractor Browser Capabilities Extensions // +//////////////////////////////////////////////// +"use strict"; + +module.exports = browser.getCapabilities().then(function(s) { + var browserName, browserVersion; + var shortName, shortVersion; + var ie, ff, ch, sa; + var platform; + platform = s.caps_.platform; + browserName = s.caps_.browserName; + browserVersion = s.caps_.version; + shortVersion = browserVersion.split('.')[0]; + ie = /i.*explore/.test(browserName); + ff = /firefox/.test(browserName); + ch = /chrome/.test(browserName); + sa = /safari/.test(browserName); + + if (ie) { + shortName = 'ie'; + } else if (ff) { + shortName = 'ff'; + } else if (ch) { + shortName = 'ch'; + } else if (sa) { + shortName = 'sa'; + } else { + throw new Exception('Unsupported browser: '+ browserName); + }; + + // Returns one of these: ['ch', 'ff', 'sa', 'ie'] + browser.getShortBrowserName = function() { + return shortName; + }; + + // Returns one of these: ['ch33', 'ff27', 'sa7', 'ie11', 'ie10', 'ie9'] + browser.getShortNameVersionAll = function() { + return shortName + shortVersion; + }; + + // Returns one of these: ['ch', 'ff', 'sa', 'ie11', 'ie10', 'ie9'] + browser.getShortNameVersion = function() { + if (ie) { + return shortName + shortVersion; + } else { + return shortName; + }; + }; + + // Return if current browser is IE, optionally specifying if it is a particular IE version + browser.isInternetExplorer = function(ver) { + if (ver == null) { + return ie; + } else { + return ie && ver.toString() === shortVersion; + } + }; + + // Function alias + browser.isIE = browser.isInternetExplorer; + + browser.isSafari = function() { + return sa; + }; + + browser.isFirefox = function() { + return ff; + }; + + // Return if current browser is Chrome, optionally specifying if it is a particular Chrome version + browser.isChrome = function(ver) { + if (ver == null) { + return ch; + } else { + return ch && ver.toString() === shortVersion; + } + }; + + browser.inWindows = function() { + return /^WIN|XP/.test(platform); + }; + + browser.inOSX = function() { + return /^MAC/.test(platform); + }; + + // Save current webdriver session id for later use + browser.webdriverRemoteSessionId = s.caps_['webdriver.remote.sessionid']; + + browser.inSauceLabs = function() { + return !!(browser.params.inSauceLabs); + }; + + browser.inBrowserStack = function() { + return !!(browser.params.inBrowserStack); + }; + + browser.inTheCloud = function() { + return !!(browser.params.inSauceLabs || browser.params.inBrowserStack); + }; +}); diff --git a/e2e/full/admin/attributes/custom-fields.e2e.js b/e2e/full/admin/attributes/custom-fields.e2e.js new file mode 100644 index 00000000..45d403df --- /dev/null +++ b/e2e/full/admin/attributes/custom-fields.e2e.js @@ -0,0 +1,193 @@ +var utils = require('../../../utils'); +var customFieldsHelper = require('../../../helpers/custom-fields-helper'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('custom-fields', function() { + before(async function() { + browser.get(browser.params.glob.host + 'project/project-3/admin/project-values/custom-fields'); + await utils.common.waitLoader(); + + utils.common.takeScreenshot('attributes', 'custom-fields'); + }); + + describe('create custom fields', function() { + describe('userstories', function() { + let typeIndex = 0; + + it('create', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + customFieldsHelper.create(typeIndex, 'test1-text', 'desc1', 1); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + customFieldsHelper.create(typeIndex, 'test1-multi', 'desc1', 3); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + // customFieldsHelper.create(typeIndex, 'test1-date', 'desc1', 4); + + // // debounce :( + // await utils.notifications.success.open(); + // await browser.sleep(2000); + + let countCustomFields = customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + expect(countCustomFields).to.be.eventually.equal(oldCountCustomFields + 2); + }); + + it('edit', async function() { + customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag', async function() { + let nameOld = await customFieldsHelper.getName(typeIndex, 0); + + await customFieldsHelper.drag(typeIndex, 0, 1); + + let nameNew = customFieldsHelper.getName(typeIndex, 1); + + expect(nameNew).to.be.eventually.equal(nameOld); + }); + + it('delete', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + customFieldsHelper.delete(typeIndex, 0); + + browser.wait(async function() { + let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + return countCustomFields === oldCountCustomFields - 1; + }, 4000); + }); + }); + + describe('tasks', function() { + let typeIndex = 1; + + it('create', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + customFieldsHelper.create(typeIndex, 'test1-text', 'desc1', 1); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + customFieldsHelper.create(typeIndex, 'test1-multi', 'desc1', 3); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + // customFieldsHelper.create(typeIndex, 'test1-date', 'desc1', 4); + + // // debounce :( + // await utils.notifications.success.open(); + // await browser.sleep(2000); + + let countCustomFields = customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + expect(countCustomFields).to.be.eventually.equal(oldCountCustomFields + 2); + }); + + it('edit', async function() { + customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag', async function() { + let nameOld = await customFieldsHelper.getName(typeIndex, 0); + + await customFieldsHelper.drag(typeIndex, 0, 1); + + let nameNew = customFieldsHelper.getName(typeIndex, 1); + + expect(nameNew).to.be.eventually.equal(nameOld); + }); + + it('delete', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + customFieldsHelper.delete(typeIndex, 0); + + browser.wait(async function() { + let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + return countCustomFields === oldCountCustomFields - 1; + }, 4000); + }); + }); + + describe('issues', function() { + let typeIndex = 2; + + it('create', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + customFieldsHelper.create(typeIndex, 'test1-text', 'desc1', 1); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + customFieldsHelper.create(typeIndex, 'test1-multi', 'desc1', 3); + + // debounce :( + await utils.notifications.success.open(); + await browser.sleep(2000); + + // customFieldsHelper.create(typeIndex, 'test1-date', 'desc1', 4); + + // // debounce :( + // await utils.notifications.success.open(); + // await browser.sleep(2000); + + let countCustomFields = customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + expect(countCustomFields).to.be.eventually.equal(oldCountCustomFields + 2); + }); + + it('edit', async function() { + customFieldsHelper.edit(typeIndex, 0, 'edit', 'desc2', 2); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag', async function() { + let nameOld = await customFieldsHelper.getName(typeIndex, 0); + + await customFieldsHelper.drag(typeIndex, 0, 1); + + let nameNew = customFieldsHelper.getName(typeIndex, 1); + + expect(nameNew).to.be.eventually.equal(nameOld); + }); + + it('delete', async function() { + let oldCountCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + customFieldsHelper.delete(typeIndex, 0); + + browser.wait(async function() { + let countCustomFields = await customFieldsHelper.getCustomFiledsByType(typeIndex).count(); + + return countCustomFields === oldCountCustomFields - 1; + }, 4000); + }); + }); + }); +}); diff --git a/e2e/full/admin/attributes/points.e2e.js b/e2e/full/admin/attributes/points.e2e.js new file mode 100644 index 00000000..1c9a5148 --- /dev/null +++ b/e2e/full/admin/attributes/points.e2e.js @@ -0,0 +1,100 @@ +var utils = require('../../../utils'); + +var adminAttributesHelper = require('../../../helpers').adminAttributes; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('attributes - points', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-values/points'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('attributes', 'points'); + }); + + it('new', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let count = await rows.count(); + + let formWrapper = section.openNew(); + + let form = adminAttributesHelper.getPointsForm(formWrapper); + + await form.name().sendKeys('test test'); + await form.value().sendKeys('2'); + + await form.save(); + + await browser.waitForAngular(); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count + 1); + }); + + it('delete', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + + let count = await rows.count(); + + let row = rows.get(count - 1); + + section.delete(row); + + let el = $('.lightbox-ask-choice'); + + await utils.lightbox.open(el); + + utils.common.takeScreenshot('attributes', 'delete-point'); + + el.$('.button-green').click(); + + await utils.lightbox.close(el); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count - 1); + }); + + it('edit', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + + let row = rows.get(0); + + await section.edit(row); + + let form = adminAttributesHelper.getPointsForm(row.$('form')); + + let newStatusName = 'test test' + Date.now(); + + await form.name().clear(); + await form.name().sendKeys(newStatusName); + await form.save(); + + await browser.waitForAngular(); + + let newStatuses = await adminAttributesHelper.getPointsNames(section.el); + + expect(newStatuses.indexOf(newStatusName)).to.be.not.equal(-1); + }); + + it.skip('drag', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let points = await adminAttributesHelper.getPointsNames(section.el); + + await utils.common.drag(rows.get(0), rows.get(2)); + + let newPoints = await adminAttributesHelper.getPointsNames(section.el); + + expect(points[0]).to.be.equal(newPoints[1]); + }); +}); diff --git a/e2e/full/admin/attributes/priorities.e2e.js b/e2e/full/admin/attributes/priorities.e2e.js new file mode 100644 index 00000000..65a4f49c --- /dev/null +++ b/e2e/full/admin/attributes/priorities.e2e.js @@ -0,0 +1,98 @@ +var utils = require('../../../utils'); + +var adminAttributesHelper = require('../../../helpers').adminAttributes; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('attributes - priorities', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-values/priorities'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('attributes', 'priorities'); + }); + + it('new priority', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let count = await rows.count(); + + let formWrapper = section.openNew(); + + let form = adminAttributesHelper.getGenericForm(formWrapper); + + await form.name().sendKeys('test test'); + + await form.save(); + + await browser.waitForAngular(); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count + 1); + }); + + it('delete', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + + let count = await rows.count(); + + let row = rows.get(count - 1); + + section.delete(row); + + let el = $('.lightbox-ask-choice'); + + await utils.lightbox.open(el); + + utils.common.takeScreenshot('attributes', 'delete-priority'); + + el.$('.button-green').click(); + + await utils.lightbox.close(el); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count - 1); + }); + + it('edit', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let row = rows.get(0); + + await section.edit(row); + + let form = adminAttributesHelper.getGenericForm(row.$('form')); + + let newPriorityName = 'test test' + Date.now(); + await form.name().clear(); + await form.name().sendKeys(newPriorityName); + + await form.save(); + + await browser.waitForAngular(); + + let newPriorities = await adminAttributesHelper.getGenericNames(section.el); + + expect(newPriorities.indexOf(newPriorityName)).to.be.not.equal(-1); + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let priorities = await adminAttributesHelper.getGenericNames(section.el); + + await utils.common.drag(rows.get(0), rows.get(2)); + + let newPriorities = await adminAttributesHelper.getGenericNames(section.el); + + expect(priorities[0]).to.be.equal(newPriorities[1]); + }); +}); diff --git a/e2e/full/admin/attributes/severities.e2e.js b/e2e/full/admin/attributes/severities.e2e.js new file mode 100644 index 00000000..cff219d0 --- /dev/null +++ b/e2e/full/admin/attributes/severities.e2e.js @@ -0,0 +1,98 @@ +var utils = require('../../../utils'); + +var adminAttributesHelper = require('../../../helpers').adminAttributes; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('attributes - severities', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-values/severities'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('attributes', 'severities'); + }); + + it('new severity', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let count = await rows.count(); + + let formWrapper = section.openNew(); + + let form = adminAttributesHelper.getGenericForm(formWrapper); + + await form.name().sendKeys('test test'); + + await form.save(); + + await browser.waitForAngular(); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count + 1); + }); + + it('delete', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + + let count = await rows.count(); + + let row = rows.get(count - 1); + + section.delete(row); + + let el = $('.lightbox-ask-choice'); + + await utils.lightbox.open(el); + + utils.common.takeScreenshot('attributes', 'delete-severity'); + + el.$('.button-green').click(); + + await utils.lightbox.close(el); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count - 1); + }); + + it('edit', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let row = rows.get(0); + + await section.edit(row); + + let form = adminAttributesHelper.getGenericForm(row.$('form')); + + let newName = 'test test' + Date.now(); + await form.name().clear(); + await form.name().sendKeys(newName); + + await form.save(); + + await browser.waitForAngular(); + + let newObjs = await adminAttributesHelper.getGenericNames(section.el); + + expect(newObjs.indexOf(newName)).to.be.not.equal(-1); + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let objs = await adminAttributesHelper.getGenericNames(section.el); + + await utils.common.drag(rows.get(0), rows.get(2)); + + let newObjs = await adminAttributesHelper.getGenericNames(section.el); + + expect(objs[0]).to.be.equal(newObjs[1]); + }); +}); diff --git a/e2e/full/admin/attributes/status.e2e.js b/e2e/full/admin/attributes/status.e2e.js new file mode 100644 index 00000000..89f5378c --- /dev/null +++ b/e2e/full/admin/attributes/status.e2e.js @@ -0,0 +1,124 @@ +var utils = require('../../../utils'); + +var adminAttributesHelper = require('../../../helpers').adminAttributes; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('attributes - status', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-values/status'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('attributes', 'status'); + }); + + it('new status', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let count = await rows.count(); + + let formWrapper = section.openNew(); + + let form = adminAttributesHelper.getStatusForm(formWrapper); + + await form.status().sendKeys('test test'); + + await form.save(); + + await browser.waitForAngular(); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count + 1); + }); + + it('duplicate status', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let count = await rows.count(); + + let formWrapper = section.openNew(); + + let form = adminAttributesHelper.getStatusForm(formWrapper); + + await form.status().sendKeys('test test'); + + await form.save(); + + await browser.waitForAngular(); + + let newCount = await rows.count(); + + let errors = await form.errors().count(); + + utils.common.takeScreenshot('attributes', 'status-error'); + + expect(errors).to.be.equal(1); + expect(newCount).to.be.equal(count); + }); + + it('delete', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + + let count = await rows.count(); + + let row = rows.get(count - 1); + + section.delete(row); + + let el = $('.lightbox-ask-choice'); + + await utils.lightbox.open(el); + + utils.common.takeScreenshot('attributes', 'delete-status'); + + el.$('.button-green').click(); + + await browser.waitForAngular(); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count - 1); + }); + + it('edit', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + + let row = rows.get(0); + + await section.edit(row); + + let form = adminAttributesHelper.getStatusForm(row.$('form')); + + let newStatusName = 'test test' + Date.now(); + + await form.status().clear(); + await form.status().sendKeys(newStatusName); + await form.save(); + + await browser.waitForAngular(); + + let newStatuses = await adminAttributesHelper.getStatusNames(section.el); + + expect(newStatuses.indexOf(newStatusName)).to.be.not.equal(-1); + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let statuses = await adminAttributesHelper.getStatusNames(section.el); + + await utils.common.drag(rows.get(0), rows.get(2)); + + let newStatuses = await adminAttributesHelper.getStatusNames(section.el); + + expect(statuses[0]).to.be.equal(newStatuses[1]); + }); +}); diff --git a/e2e/full/admin/attributes/types.e2e.js b/e2e/full/admin/attributes/types.e2e.js new file mode 100644 index 00000000..c6d3b309 --- /dev/null +++ b/e2e/full/admin/attributes/types.e2e.js @@ -0,0 +1,98 @@ +var utils = require('../../../utils'); + +var adminAttributesHelper = require('../../../helpers').adminAttributes; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('attributes - types', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-values/types'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('attributes', 'types'); + }); + + it('new type', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let count = await rows.count(); + + let formWrapper = section.openNew(); + + let form = adminAttributesHelper.getGenericForm(formWrapper); + + await form.name().sendKeys('test test'); + + await form.save(); + + await browser.waitForAngular(); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count + 1); + }); + + it('delete', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + + let count = await rows.count(); + + let row = rows.get(count - 1); + + section.delete(row); + + let el = $('.lightbox-ask-choice'); + + await utils.lightbox.open(el); + + utils.common.takeScreenshot('attributes', 'delete-type'); + + el.$('.button-green').click(); + + await utils.lightbox.close(el); + + let newCount = await rows.count(); + + expect(newCount).to.be.equal(count - 1); + }); + + it('edit', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let row = rows.get(0); + + await section.edit(row); + + let form = adminAttributesHelper.getGenericForm(row.$('form')); + + let newName = 'test test' + Date.now(); + await form.name().clear(); + await form.name().sendKeys(newName); + + await form.save(); + + await browser.waitForAngular(); + + let newObjs = await adminAttributesHelper.getGenericNames(section.el); + + expect(newObjs.indexOf(newName)).to.be.not.equal(-1); + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag', async function() { + let section = adminAttributesHelper.getSection(0); + let rows = section.rows(); + let objs = await adminAttributesHelper.getGenericNames(section.el); + + await utils.common.drag(rows.get(0), rows.get(2)); + + let newObjs = await adminAttributesHelper.getGenericNames(section.el); + + expect(objs[0]).to.be.equal(newObjs[1]); + }); +}); diff --git a/e2e/full/admin/integrations/bitbucket.e2e.js b/e2e/full/admin/integrations/bitbucket.e2e.js new file mode 100644 index 00000000..fd3ae446 --- /dev/null +++ b/e2e/full/admin/integrations/bitbucket.e2e.js @@ -0,0 +1,25 @@ +var utils = require('../../../utils'); + +var adminIntegrationsHelper = require('../../../helpers').adminIntegrations; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('admin - bitbucket', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-3/admin/third-parties/bitbucket'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('integrations', 'bitbucket'); + }); + + it('save', async function() { + $('.submit-button').click(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); +}); diff --git a/e2e/full/admin/integrations/github.e2e.js b/e2e/full/admin/integrations/github.e2e.js new file mode 100644 index 00000000..e1f35a77 --- /dev/null +++ b/e2e/full/admin/integrations/github.e2e.js @@ -0,0 +1,25 @@ +var utils = require('../../../utils'); + +var adminIntegrationsHelper = require('../../../helpers').adminIntegrations; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('admin - github', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-3/admin/third-parties/github'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('integrations', 'github'); + }); + + it('save', async function() { + $('.submit-button').click(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); +}); diff --git a/e2e/full/admin/integrations/gitlab.e2e.js b/e2e/full/admin/integrations/gitlab.e2e.js new file mode 100644 index 00000000..f3b185b0 --- /dev/null +++ b/e2e/full/admin/integrations/gitlab.e2e.js @@ -0,0 +1,25 @@ +var utils = require('../../../utils'); + +var adminIntegrationsHelper = require('../../../helpers').adminIntegrations; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('admin - gitlab', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-3/admin/third-parties/gitlab'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('integrations', 'gitlab'); + }); + + it('save', async function() { + $('.submit-button').click(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); +}); diff --git a/e2e/full/admin/integrations/webhooks.e2e.js b/e2e/full/admin/integrations/webhooks.e2e.js new file mode 100644 index 00000000..1833faaa --- /dev/null +++ b/e2e/full/admin/integrations/webhooks.e2e.js @@ -0,0 +1,67 @@ +var utils = require('../../../utils'); + +var adminIntegrationsHelper = require('../../../helpers').adminIntegrations; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('admin - webhooks', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-3/admin/third-parties/webhooks'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('integrations', 'webhooks'); + }); + + it('error - empty', async function() { + await adminIntegrationsHelper.saveWebHook('', '', ''); + + utils.common.takeScreenshot('integrations', 'webhooks-errors'); + + let errorsCount = await adminIntegrationsHelper.getErrors().count(); + + expect(errorsCount).to.be.equal(3); + }); + + it('error - invalid url', async function() { + await adminIntegrationsHelper.saveWebHook('ooo', 'iii', 'uuuu'); + + utils.common.takeScreenshot('integrations', 'webhooks-invalid-url'); + + let errorsCount = await adminIntegrationsHelper.getErrors().count(); + + expect(errorsCount).to.be.equal(1); + }); + + it('valid', async function() { + await adminIntegrationsHelper.saveWebHook('ooo', 'http://web.fake', 'uuuu'); + + utils.common.takeScreenshot('integrations', 'webhooks-invalid-url'); + + let webHookIsPresent = await adminIntegrationsHelper.currentWebHookIsPresent(); + + expect(webHookIsPresent).to.be.true; + }); + + it('edit', async function() { + adminIntegrationsHelper.openEditModeWebHook(); + + await adminIntegrationsHelper.saveWebHook('111', 'http://web2.fake', '333'); + + let webHookMode = await adminIntegrationsHelper.getWebHookMode(); + + expect(webHookMode).to.be.equal('read'); + }); + + it('delete', async function() { + await adminIntegrationsHelper.deleteWebhook(); + + let webHookIsPresent = await adminIntegrationsHelper.currentWebHookIsPresent(); + + expect(webHookIsPresent).to.be.false; + }); +}); diff --git a/e2e/full/admin/members.e2e.js b/e2e/full/admin/members.e2e.js new file mode 100644 index 00000000..77618ee1 --- /dev/null +++ b/e2e/full/admin/members.e2e.js @@ -0,0 +1,142 @@ +var utils = require('../../utils'); + +var adminMembershipsHelper = require('../../helpers').adminMemberships; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('admin - members', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/memberships'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('memberships', 'memberships'); + }); + + describe('new member', async function() { + let initMembersCount = 0; + let newMemberLightbox = null; + + before(async function() { + initMembersCount = await adminMembershipsHelper.getMembers().count(); + + newMemberLightbox = adminMembershipsHelper.getNewMemberLightbox(); + adminMembershipsHelper.openNewMemberLightbox(); + + await newMemberLightbox.waitOpen(); + utils.common.takeScreenshot('memberships', 'new-member'); + }); + + it('add members row', async function() { + newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es'); + newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es'); + newMemberLightbox.newEmail('xxx' + new Date().getTime() + '@xx.es'); + + let membersRows = await newMemberLightbox.getRows().count(); + + expect(membersRows).to.be.equal(3 + 1); + }); + + it('delete members row', async function() { + newMemberLightbox.deleteRow(2); + + let membersRows = await newMemberLightbox.getRows().count(); + + expect(membersRows).to.be.equal(2 + 1); + }); + + it('submit', async function() { + newMemberLightbox.submit(); + + await newMemberLightbox.waitClose(); + + let members = adminMembershipsHelper.getMembers(); + let membersCount = await members.count(); + + expect(membersCount).to.be.equal(initMembersCount + 2); + }); + + it('the last two should be pending', async function() { + let members = adminMembershipsHelper.getMembers(); + let membersCount = await members.count(); + + let lastMember1 = members.get(membersCount - 1); + let lastMember2 = members.get(membersCount - 2); + + let active1 = await adminMembershipsHelper.isActive(lastMember1); + let active2 = await adminMembershipsHelper.isActive(lastMember2); + + expect(active1).to.be.false; + expect(active2).to.be.false; + }); + }); + + it('delete member', async function() { + let initMembersCount = await adminMembershipsHelper.getMembers().count(); + + let member = adminMembershipsHelper.getMembers().last(); + + adminMembershipsHelper.delete(member); + + utils.common.takeScreenshot('memberships', 'delete-member-lb'); + + await utils.lightbox.confirm.ok(); + + let membersCount = await adminMembershipsHelper.getMembers().count(); + + expect(membersCount).to.be.equal(initMembersCount - 1); + }); + + it('change role', async function() { + let member = adminMembershipsHelper.getMembers().last(); + + //prevent change to the same value + adminMembershipsHelper.setRole(member, 1); + adminMembershipsHelper.setRole(member, 3); + adminMembershipsHelper.setRole(member, 2); + + expect(utils.notifications.success.open()).to.be.eventually.true; + + await utils.notifications.success.close(); + }); + + it('resend invitation', async function() { + let member = adminMembershipsHelper.getMembers().last(); + + adminMembershipsHelper.sendInvitation(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + + await utils.notifications.success.close(); + }); + + it('toggle admin', async function() { + let member = adminMembershipsHelper.getMembers().get(1); + let isAdmin = await adminMembershipsHelper.isAdmin(member); + + if (isAdmin) { + adminMembershipsHelper.toggleAdmin(member); + + await utils.notifications.success.open(); + + isAdmin = await adminMembershipsHelper.isAdmin(member); + + expect(isAdmin).not.to.be.true; + } else { + adminMembershipsHelper.toggleAdmin(member); + + await utils.notifications.success.open(); + + isAdmin = await adminMembershipsHelper.isAdmin(member); + + expect(isAdmin).to.be.true; + } + + await utils.notifications.success.close(); + }); + +}); diff --git a/e2e/full/admin/permissions.e2e.js b/e2e/full/admin/permissions.e2e.js new file mode 100644 index 00000000..c1416f18 --- /dev/null +++ b/e2e/full/admin/permissions.e2e.js @@ -0,0 +1,80 @@ +var utils = require('../../utils'); + +var adminPermissionsHelper = require('../../helpers').adminPermissions; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('admin - roles', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/roles'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('permissions', 'permissions'); + }); + + it('new role', async function() { + let oldRolesCount = await adminPermissionsHelper.getRoles().count(); + + await adminPermissionsHelper.newRole('test'); + + let newRolesCount = await adminPermissionsHelper.getRoles().count(); + + expect(newRolesCount).to.be.equal(oldRolesCount + 1); + }); + + it('edit role name', async function() { + await adminPermissionsHelper.editRole('2'); + + expect(utils.notifications.success.open()).to.be.eventually.true; + + await utils.notifications.success.close(); + }); + + it('toggle, estimation role', async function() { + adminPermissionsHelper.toggleEstimationRole(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + + await utils.notifications.success.close(); + }); + + it('toggle, category permission', async function() { + await adminPermissionsHelper.openCategory(0); + + let permission = await adminPermissionsHelper.getPermissionsCategory(0).get(0); + let oldValue = await adminPermissionsHelper.getCategoryPermissionValue(permission); + + adminPermissionsHelper.toggleCategoryPermission(permission); + + await utils.notifications.success.open(); + + let newValue = await adminPermissionsHelper.getCategoryPermissionValue(permission); + + expect(newValue).not.be.equal(oldValue); + + await utils.notifications.success.close(); + }); + + it('delete', async function() { + let oldRolesCount = await adminPermissionsHelper.getRoles().count(); + + adminPermissionsHelper.delete(); + + let el = $('.lightbox-ask-choice'); + + await utils.lightbox.open(el); + + utils.common.takeScreenshot('attributes', 'delete-type'); + + el.$('.button-green').click(); + + let newRolesCount = await adminPermissionsHelper.getRoles().count(); + + expect(newRolesCount).to.be.equal(oldRolesCount - 1); + }); +}); diff --git a/e2e/full/admin/project/create-delete.e2e.js b/e2e/full/admin/project/create-delete.e2e.js new file mode 100644 index 00000000..631704a5 --- /dev/null +++ b/e2e/full/admin/project/create-delete.e2e.js @@ -0,0 +1,67 @@ +var utils = require('../../../utils'); +var createProject = require('../../../helpers').createProject; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('create-delete project', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'projects/'); + + await utils.common.waitLoader(); + }); + + let lb; + + before(async function() { + lb = createProject.createProjectLightbox(); + + createProject.openWizard(); + + await lb.waitOpen(); + }); + + it('create - step 1', async function() { + utils.common.takeScreenshot('project-wizard', 'step1'); + + await lb.next(); + }); + + it('create - step 2 errors', async function() { + utils.common.takeScreenshot('project-wizard', 'step2'); + + await lb.submit(); + + utils.common.takeScreenshot('project-wizard', 'step2-error'); + + let errors = await lb.errors().count(); + + expect(errors).to.be.equal(2); + }); + + it('create - step 2', async function() { + lb.name().sendKeys('aaa'); + lb.description().sendKeys('bbb'); + + await lb.submit(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + + it('delete', async function() { + let linkAdmin = $('#nav-admin a'); + utils.common.link(linkAdmin); + + await utils.common.waitLoader(); + + await createProject.delete(); + await browser.waitForAngular(); + + let url = await browser.getCurrentUrl(); + + expect(url).to.be.equal(browser.params.glob.host); + }); +}); diff --git a/e2e/full/admin/project/default-values.e2e.js b/e2e/full/admin/project/default-values.e2e.js new file mode 100644 index 00000000..f166579b --- /dev/null +++ b/e2e/full/admin/project/default-values.e2e.js @@ -0,0 +1,27 @@ +var utils = require('../../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('project default values', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-profile/default-values'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('admin', 'project-default-values'); + }); + + it('change serveral default values and save it', async function() { + let fieldsets = $$('.default-values fieldset'); + + fieldsets.get(0).$(`select option:nth-child(2)`).click(); + fieldsets.get(1).$(`select option:nth-child(2)`).click(); + + $('button[type="submit"]').click(); + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + }); +}); diff --git a/e2e/full/admin/project/modules.e2e.js b/e2e/full/admin/project/modules.e2e.js new file mode 100644 index 00000000..42ea3c3d --- /dev/null +++ b/e2e/full/admin/project/modules.e2e.js @@ -0,0 +1,83 @@ +var utils = require('../../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('modules', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-profile/modules'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('admin', 'project-modules'); + }); + + it('disable module', async function() { + let functionalities = $$('.functionality'); + + let functionality = functionalities.get(0); + + let label = functionality.$('label'); + + browser.actions() + .mouseMove(label) + .click() + .perform(); + + $('button[type="submit"]').click(); + + let active = await utils.common.hasClass(functionality, 'active'); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + expect(active).to.be.false; + + await utils.notifications.success.close(); + }); + + it('enable module', async function() { + let functionalities = $$('.functionality'); + + let functionality = functionalities.get(0); + + let label = functionality.$('label'); + + browser.actions() + .mouseMove(label) + .click() + .perform(); + + $('button[type="submit"]').click(); + + let active = await utils.common.hasClass(functionality, 'active'); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + expect(active).to.be.true; + + await utils.notifications.success.close(); + }); + + it('enable videoconference', async function() { + let functionality = $$('.functionality').get(4); + + let label = functionality.$('label'); + + browser.actions() + .mouseMove(label) + .click() + .perform(); + + let videoconference = functionality.$$('select').get(0); + + videoconference.$(`option:nth-child(1)`).click(); + + let salt = functionality.$$('select').get(0); + + salt.sendKeys('abccceee'); + + $('button[type="submit"]').click(); + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + }); +}); diff --git a/e2e/full/admin/project/project-detail.e2e.js b/e2e/full/admin/project/project-detail.e2e.js new file mode 100644 index 00000000..827b562c --- /dev/null +++ b/e2e/full/admin/project/project-detail.e2e.js @@ -0,0 +1,44 @@ +var utils = require('../../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('project detail', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-profile/details'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('admin', 'project-detail'); + }); + + it('edit tag, description and project settings', async function() { + let tag = $('.tag-input'); + + tag.sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + tag.sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + let description = $('#project-description'); + + description.sendKeys('test test'); + + let privateProjectButton = $$('.trans-button').get(1); + + browser.actions() + .mouseMove(privateProjectButton) + .click() + .perform(); + + utils.common.takeScreenshot('admin', 'project-detail-filled'); + + $('button[type="submit"]').click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + }); +}); diff --git a/e2e/full/admin/project/reports.e2e.js b/e2e/full/admin/project/reports.e2e.js new file mode 100644 index 00000000..d37f7d51 --- /dev/null +++ b/e2e/full/admin/project/reports.e2e.js @@ -0,0 +1,44 @@ +var utils = require('../../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('reports', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/admin/project-profile/reports'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('admin', 'project-reports'); + }); + + it('generate report url', async function() { + let reportField = $$('.csv-regenerate-field').get(0); + + reportField.$('a').click(); + + await browser.waitForAngular(); + + let value = await reportField.$('input').getAttribute('value'); + + expect(value).to.have.length.above(1); + }); + + it('regenerate report url', async function() { + let reportField = $$('.csv-regenerate-field').get(0); + let oldValue = await reportField.$('input').getAttribute('value'); + + reportField.$('a').click(); + + await utils.lightbox.confirm.ok(); + + await browser.waitForAngular(); + + let value = await reportField.$('input').getAttribute('value'); + + expect(value).not.to.be.equal(oldValue); + }); +}); diff --git a/e2e/full/backlog.e2e.js b/e2e/full/backlog.e2e.js new file mode 100644 index 00000000..173d506d --- /dev/null +++ b/e2e/full/backlog.e2e.js @@ -0,0 +1,627 @@ +var utils = require('../utils'); +var backlogHelper = require('../helpers').backlog; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('backlog', function() { + before(async function() { + browser.get(browser.params.glob.host + 'project/project-3/backlog'); + await utils.common.waitLoader(); + + utils.common.takeScreenshot('backlog', 'backlog'); + }); + + describe('create US', function() { + let createUSLightbox = null; + + before(async function() { + backlogHelper.openNewUs(); + + createUSLightbox = backlogHelper.getCreateEditUsLightbox(); + await createUSLightbox.waitOpen(); + }); + + it('capture screen', function() { + utils.common.takeScreenshot('backlog', 'create-us'); + }); + + it('fill form', async function() { + // subject + createUSLightbox.subject().sendKeys('subject'); + + // roles + createUSLightbox.setRole(1, 3); + createUSLightbox.setRole(3, 4); + + let totalPoints = await createUSLightbox.getRolePoints(); + + expect(totalPoints).to.be.equal('3'); + + // status + createUSLightbox.status(2).click(); + + // tags + createUSLightbox.tags().sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + createUSLightbox.tags().sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + // description + createUSLightbox.description().sendKeys('test test'); + + //settings + createUSLightbox.settings(0).click(); + + + await utils.common.waitTransitionTime(createUSLightbox.settings(0)); + + utils.common.takeScreenshot('backlog', 'create-us-filled'); + }); + + it('send form', async function() { + let usCount = await backlogHelper.userStories().count(); + + createUSLightbox.submit(); + + await utils.lightbox.close(createUSLightbox.el); + + let newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.equal(usCount + 1); + }); + }); + + describe('bulk create US', function() { + let createUSLightbox = null; + + before(async function() { + backlogHelper.openBulk(); + + createUSLightbox = backlogHelper.getBulkCreateLightbox(); + + await createUSLightbox.waitOpen(); + }); + + it('fill form', function() { + createUSLightbox.textarea().sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + createUSLightbox.textarea().sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + }); + + it('send form', async function() { + let usCount = await backlogHelper.userStories().count(); + + createUSLightbox.submit(); + + await createUSLightbox.waitClose(); + + let newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.equal(usCount + 2); + }); + }); + + describe('edit US', function() { + let editUSLightbox = null; + + before(async function() { + backlogHelper.openUsBacklogEdit(0); + + editUSLightbox = backlogHelper.getCreateEditUsLightbox(); + + await editUSLightbox.waitOpen(); + }); + + it('fill form', async function() { + // subject + editUSLightbox.subject().sendKeys('subjectedit'); + + // roles + editUSLightbox.setRole(1, 3); + editUSLightbox.setRole(2, 3); + editUSLightbox.setRole(3, 3); + editUSLightbox.setRole(4, 3); + + let totalPoints = await editUSLightbox.getRolePoints(); + + expect(totalPoints).to.be.equal('4'); + + // status + editUSLightbox.status(3).click(); + + // tags + editUSLightbox.tags().sendKeys('www'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + editUSLightbox.tags().sendKeys('xxx'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + // description + editUSLightbox.description().sendKeys('test test test test'); + + //settings + editUSLightbox.settings(1).click(); + }); + + it('send form', async function() { + editUSLightbox.submit(); + + await editUSLightbox.waitClose(); + }); + }); + + it('edit status inline', async function() { + await backlogHelper.setUsStatus(0, 1); + + // debounce + await browser.sleep(2000); + + let statusText = await backlogHelper.setUsStatus(0, 2); + + expect(statusText).to.be.equal('In progress'); + }); + + it('edit points inline', async function() { + await backlogHelper.setUsPoints(0, 1, 1); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + + it('delete US', async function() { + let usCount = await backlogHelper.userStories().count(); + + backlogHelper.deleteUs(0); + + await utils.lightbox.confirm.ok(); + + let newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.equal(usCount - 1); + }); + + it('drag backlog us', async function() { + let dragableElements = backlogHelper.userStories(); + + let dragElement = dragableElements.get(1); + let dragElementHandler = dragElement.$('.icon-drag-v'); + + let draggedElementRef = await backlogHelper.getUsRef(dragElement); + + await utils.common.drag(dragElementHandler, dragableElements.get(0)); + await browser.waitForAngular(); + + let firstElementTextRef = await backlogHelper.getUsRef(dragableElements.get(0)); + + expect(firstElementTextRef).to.be.equal(draggedElementRef); + }); + + it('reorder multiple us', async function() { + let dragableElements = backlogHelper.userStories(); + + let count = await dragableElements.count(); + + let draggedRefs = []; + + //element 1 + let dragElement = dragableElements.get(count - 1); + dragElement.$('input[type="checkbox"]').click(); + let ref1 = await backlogHelper.getUsRef(dragElement); + draggedRefs.push(await backlogHelper.getUsRef(dragElement)); + + //element 2 + dragElement = dragableElements.get(count - 2); + dragElement.$('input[type="checkbox"]').click(); + let ref2 = await backlogHelper.getUsRef(dragElement); + draggedRefs.push(await backlogHelper.getUsRef(dragElement)); + + await utils.common.drag(dragElement, dragableElements.get(0)); + await browser.sleep(200); + + let elementRef1 = await backlogHelper.getUsRef(dragableElements.get(0)); + let elementRef2 = await backlogHelper.getUsRef(dragableElements.get(1)); + + expect(elementRef2).to.be.equal(draggedRefs[0]); + expect(elementRef1).to.be.equal(draggedRefs[1]); + }); + + it('drag multiple us to milestone', async function() { + let sprint = backlogHelper.sprints().get(0); + let initUssSprintCount = await backlogHelper.getSprintUsertories(sprint).count(); + + let dragableElements = backlogHelper.userStories(); + + let draggedRefs = []; + + // the us 1 and 2 are selected on the previous test + + let dragElement = dragableElements.get(0); + let dragElementHandler = dragElement.$('.icon-drag-v'); + + await utils.common.drag(dragElementHandler, sprint); + await browser.waitForAngular(); + + let ussSprintCount = await backlogHelper.getSprintUsertories(sprint).count(); + + expect(ussSprintCount).to.be.equal(initUssSprintCount + 2); + }); + + it('drag us to milestone', async function() { + let sprint = backlogHelper.sprints().get(0); + + let dragableElements = backlogHelper.userStories(); + let dragElement = dragableElements.get(0); + let dragElementHandler = dragElement.$('.icon-drag-v'); + + let draggedElementRef = await backlogHelper.getUsRef(dragElement); + + let initUssSprintCount = await backlogHelper.getSprintUsertories(sprint).count(); + + await utils.common.drag(dragElementHandler, sprint); + await browser.waitForAngular(); + + let ussSprintCount = await backlogHelper.getSprintUsertories(sprint).count(); + + expect(ussSprintCount).to.be.equal(initUssSprintCount + 1); + }); + + it('move to current sprint button', async function() { + let dragableElements = backlogHelper.userStories(); + let count = await dragableElements.count(); + let dragElement = dragableElements.get(count - 1); + + dragElement.$('input[type="checkbox"]').click(); + + let draggedRef = await backlogHelper.getUsRef(dragElement); + + $('#move-to-current-sprint').click(); + + let sprint = backlogHelper.sprintsOpen().last(); + + let sprintRefs = await backlogHelper.getSprintsRefs(sprint); + + expect(sprintRefs.indexOf(draggedRef)).to.be.not.equal(-1); + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'reorder milestone us', async function() { + let sprint = backlogHelper.sprints().get(0); + let dragableElements = backlogHelper.getSprintUsertories(sprint); + + let dragElement = await dragableElements.get(3); + let draggedElementRef = await backlogHelper.getUsRef(dragElement); + + await utils.common.drag(dragElement, dragableElements.get(0)); + await browser.waitForAngular(); + + let firstElementRef = await backlogHelper.getUsRef(dragableElements.get(0)); + + expect(firstElementRef).to.be.equal(firstElementRef); + }); + + utils.common.browserSkip(['firefox', 'internet explorer'], 'drag us from milestone to milestone', async function() { + let sprint1 = backlogHelper.sprints().get(0); + let sprint2 = backlogHelper.sprints().get(1); + + let initUssSprintCount = await backlogHelper.getSprintUsertories(sprint2).count(); + + let dragElement = backlogHelper.getSprintUsertories(sprint1).get(0); + + await utils.common.drag(dragElement, sprint2); + await browser.waitForAngular(); + + let firstElement = backlogHelper.getSprintUsertories(sprint2).get(0); + + let ussSprintCount = await backlogHelper.getSprintUsertories(sprint2).count(); + + expect(ussSprintCount).to.be.equal(initUssSprintCount + 1); + }); + + utils.common.browserSkip('internet explorer', 'select us with SHIFT', async function() { + let dragableElements = backlogHelper.userStories(); + + let firstInput = dragableElements.get(0).$('input[type="checkbox"]'); + let lastInput = dragableElements.get(3).$('input[type="checkbox"]'); + + browser.actions() + .mouseMove(firstInput) + .keyDown(protractor.Key.SHIFT) + .click() + .mouseMove(lastInput) + .click() + .perform(); + + let count = await backlogHelper.selectedUserStories().count(); + + expect(count).to.be.equal(4); + }); + + describe('milestones', function() { + it('create', async function() { + backlogHelper.openNewMilestone(); + + let createMilestoneLightbox = backlogHelper.getCreateEditMilestone(); + + await createMilestoneLightbox.waitOpen(); + + utils.common.takeScreenshot('backlog', 'create-milestone'); + + let sprintName = 'sprintName' + new Date().getTime(); + + createMilestoneLightbox.name().sendKeys(sprintName); + + createMilestoneLightbox.submit(); + await browser.waitForAngular(); + + // debounce + await browser.sleep(2000); + + let sprintTitles = await backlogHelper.getSprintsTitles(); + + expect(sprintTitles.indexOf(sprintName)).to.be.not.equal(-1); + }); + + it('edit', async function() { + backlogHelper.openMilestoneEdit(0); + + let createMilestoneLightbox = backlogHelper.getCreateEditMilestone(); + + await createMilestoneLightbox.waitOpen(); + + await createMilestoneLightbox.name().clear(); + + let sprintName = 'sprintName' + new Date().getTime(); + + createMilestoneLightbox.name().sendKeys(sprintName); + + createMilestoneLightbox.submit(); + await browser.waitForAngular(); + + let sprintTitles = await backlogHelper.getSprintsTitles(); + + expect(sprintTitles.indexOf(sprintName)).to.be.not.equal(-1); + }); + + it('delete', async function() { + backlogHelper.openMilestoneEdit(0); + + let createMilestoneLightbox = backlogHelper.getCreateEditMilestone(); + + await createMilestoneLightbox.waitOpen(); + + createMilestoneLightbox.delete(); + + await utils.lightbox.confirm.ok(); + await browser.waitForAngular(); + + let sprintName = createMilestoneLightbox.name().getAttribute('value'); + let sprintTitles = await backlogHelper.getSprintsTitles(); + + expect(sprintTitles.indexOf(sprintName)).to.be.equal(-1); + }); + }); + + describe('tags', function() { + it('show', function() { + $('#show-tags').click(); + + utils.common.takeScreenshot('backlog', 'backlog-tags'); + + let tag = $$('.backlog-table .tag').get(0); + + expect(tag.isDisplayed()).to.be.eventually.true; + }); + + it('hide', function() { + $('#show-tags').click(); + + let tag = $$('.backlog-table .tag').get(0); + + expect(tag.isDisplayed()).to.be.eventually.false; + }); + }); + + describe('filters', function() { + it('show filters', async function() { + let transition = utils.common.transitionend('.menu-secondary.filters-bar', 'opacity'); + + $('#show-filters-button').click(); + + await transition(); + + utils.common.takeScreenshot('backlog', 'backlog-filters'); + }); + + it('filter by subject', async function() { + let usCount = await backlogHelper.userStories().count(); + let filterQ = element(by.model('filtersQ')); + + let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + await filterQ.sendKeys('add'); + + await htmlChanges(); + + let newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.below(usCount); + + htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + // clear status + await filterQ.clear(); + + await htmlChanges(); + }); + + it('filter by ref', async function() { + let userstories = backlogHelper.userStories(); + let filterQ = element(by.model('filtersQ')); + let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + let ref = await backlogHelper.getTestingFilterRef(); + + ref = ref.replace('#', ''); + + await filterQ.sendKeys(ref); + await htmlChanges(); + + let newUsCount = await userstories.count(); + expect(newUsCount).to.be.equal(1); + + htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + // clear status + await filterQ.clear(); + + await htmlChanges(); + }); + + it('filter by status', async function() { + let usCount = await backlogHelper.userStories().count(); + + let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + $$('.filters-cats a').first().click(); + $$('.filter-list a').first().click(); + + await htmlChanges(); + + let newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.below(usCount); + + //remove status + htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + $$('.filters-applied a').first().click(); + + await htmlChanges(); + + newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.equal(usCount); + + backlogHelper.goBackFilters(); + }); + + it('filter by tags', async function() { + let usCount = await backlogHelper.userStories().count(); + let htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + $$('.filters-cats a').get(1).click(); + await browser.waitForAngular(); + + $$('.filter-list a').first().click(); + + await htmlChanges(); + + let newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.below(usCount); + + //remove tags + htmlChanges = await utils.common.outerHtmlChanges('.backlog-table-body'); + + $$('.filters-applied a').first().click(); + + await htmlChanges(); + + newUsCount = await backlogHelper.userStories().count(); + + expect(newUsCount).to.be.equal(usCount); + }); + + it('trying drag with filters open', async function() { + let dragableElements = backlogHelper.userStories(); + let dragElement = dragableElements.get(5); + + await utils.common.drag(dragElement, dragableElements.get(0)); + + expect(utils.notifications.error.open()).to.be.eventually.true; + + await utils.notifications.error.close(); + }); + + it('hide filters', async function() { + let menu = $('.menu-secondary.filters-bar'); + let transition = utils.common.transitionend('.menu-secondary.filters-bar', 'width'); + + $('#show-filters-button').click(); + + await transition(); + + expect(menu.getCssValue('width')).to.be.eventually.equal('0px'); + }); + }); + + describe('closed sprints', function() { + async function createEmptyMilestone() { + backlogHelper.openNewMilestone(); + + let createMilestoneLightbox = backlogHelper.getCreateEditMilestone(); + + await createMilestoneLightbox.waitOpen(); + + createMilestoneLightbox.name().sendKeys('sprintName' + new Date().getTime()); + createMilestoneLightbox.submit(); + + return createMilestoneLightbox.waitClose(); + } + + async function dragClosedUsToMilestone() { + await backlogHelper.setUsStatus(2, 5); + + let dragElement = backlogHelper.userStories().get(2); + let dragElementHandler = dragElement.$('.icon-drag-v'); + + let sprint = backlogHelper.sprints().last(); + await utils.common.drag(dragElementHandler, sprint); + + return browser.waitForAngular(); + } + + before(async function() { + await createEmptyMilestone(); + await dragClosedUsToMilestone(); + }); + + it('open closed sprints', async function() { + backlogHelper.toggleClosedSprints(); + + let closedSprints = await backlogHelper.closedSprints().count(); + + expect(closedSprints).to.be.equal(1); + }); + + it('close closed sprints', async function() { + backlogHelper.toggleClosedSprints(); + + let closedSprints = await backlogHelper.closedSprints().count(); + + expect(closedSprints).to.be.equal(0); + }); + + it('open sprint by drag open US to closed sprint', async function() { + backlogHelper.toggleClosedSprints(); + + await backlogHelper.setUsStatus(1, 0); + + let dragElement = backlogHelper.userStories().get(0); + let dragElementHandler = dragElement.$('.icon-drag-v'); + + let sprint = backlogHelper.sprints().last(); + await utils.common.drag(dragElementHandler, sprint); + await browser.waitForAngular(); + + let closedSprints = await backlogHelper.closedSprints().count(); + + expect(closedSprints).to.be.equal(0); + }); + }); +}); diff --git a/e2e/full/home.e2e.js b/e2e/full/home.e2e.js new file mode 100644 index 00000000..1b659bfd --- /dev/null +++ b/e2e/full/home.e2e.js @@ -0,0 +1,95 @@ +var utils = require('../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('home', function() { + before(async function(){ + browser.get(browser.params.glob.host); + + await utils.common.waitLoader(); + utils.common.takeScreenshot("home", "dashboard"); + }); + + it('working on filled', function() { + return expect($$('.working-on div[tg-duty]').count()).to.be.eventually.above(0); + }); + + it('watching filled', function() { + return expect($$('.watching div[tg-duty]').count()).to.be.eventually.above(0); + }); + + it('project list filled', function() { + return expect($$('.home-project-list-single').count()).to.be.eventually.above(0); + }); + + describe('projects list', function() { + before(async function() { + browser.get(browser.params.glob.host + 'projects/'); + + await utils.common.waitLoader(); + utils.common.takeScreenshot("home", "projects"); + }); + + it('open create project lightbox', function() { + $('.master .create-project-btn').click(); + + return expect(utils.lightbox.open('div[tg-lb-create-project]')).to.be.eventually.equal(true); + }); + + it('close create project lightbox', function() { + $('div[tg-lb-create-project] .icon-delete').click(); + + return expect(utils.lightbox.close('div[tg-lb-create-project]')).to.be.eventually.equal(true); + }); + }); + + describe("project drag and drop", function() { + var draggedElementText; + + before(async function() { + browser.get(browser.params.glob.host + 'projects/'); + + let dragableElements = element.all(by.css('.list-itemtype-project')); + let dragElement = dragableElements.get(3); + let dragElementLink = dragElement.element(by.css('a')); + + await utils.common.waitLoader(); + + draggedElementText = await dragElementLink.getText(); + + await utils.common.drag(dragElement, dragableElements.get(0)); + await browser.waitForAngular(); + }); + + utils.common.browserSkip('firefox', 'projects list has the new order', function() { + var firstElement = $$('.list-itemtype-project a').first().getText(); + + expect(firstElement).to.be.eventually.equal(draggedElementText); + }); + + utils.common.browserSkip('firefox', 'projects menu has the new order', function() { + var firstElementText = $$('div[tg-dropdown-project-list] ul a').first().getInnerHtml(); + + expect(firstElementText).to.be.eventually.equal(draggedElementText); + }); + + after(async function() { + //restore project position + let dragableElements = element.all(by.css('.list-itemtype-project')); + let dragElement = dragableElements.get(0); + let dragElementLink = dragElement.element(by.css('a')); + + await utils.common.waitLoader(); + + draggedElementText = await dragElementLink.getText(); + + await utils.common.drag(dragElement, dragableElements.get(3)); + await browser.waitForAngular(); + }); + + }); +}); diff --git a/e2e/full/issues/issue-detail.e2e.js b/e2e/full/issues/issue-detail.e2e.js new file mode 100644 index 00000000..f5486f83 --- /dev/null +++ b/e2e/full/issues/issue-detail.e2e.js @@ -0,0 +1,59 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('Issue detail', async function(){ + let issueUrl = ''; + + before(async function(){ + await utils.nav + .init() + .project('Project Example 0') + .issues() + .issue(0) + .go(); + + issueUrl = await browser.getCurrentUrl(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("issues", "detail"); + }); + + it('title edition', utils.detail.titleTesting); + + it('tags edition', utils.detail.tagsTesting); + + it('description edition', utils.detail.descriptionTesting); + + it('status edition', utils.detail.statusTesting); + + it('assigned to edition', utils.detail.assignedToTesting); + + it('watchers edition', utils.detail.watchersTesting); + + it('history', utils.detail.historyTesting); + + it('block', utils.detail.blockTesting); + + it('attachments', utils.detail.attachmentTesting); + + describe('custom-fields', utils.detail.customFields.bind(this, 2)); + + it('screenshot', async function() { + await utils.common.takeScreenshot("issues", "detail updated"); + }); + + describe('delete & redirect', function() { + it('delete', utils.detail.deleteTesting); + + it('redirected', async function (){ + let url = await browser.getCurrentUrl(); + expect(url).not.to.be.equal(issueUrl); + }); + }); +}); diff --git a/e2e/full/issues/issues.e2e.js b/e2e/full/issues/issues.e2e.js new file mode 100644 index 00000000..199e1b1f --- /dev/null +++ b/e2e/full/issues/issues.e2e.js @@ -0,0 +1,304 @@ +var utils = require('../../utils'); +var issuesHelper = require('../../helpers').issues; +var commonHelper = require('../../helpers').common; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('issues list', function() { + before(async function() { + browser.get(browser.params.glob.host + 'project/project-3/issues'); + await utils.common.waitLoader(); + + utils.common.takeScreenshot('issues', 'issues'); + }); + + describe('create Issue', function() { + let createIssueLightbox = null; + + before(async function() { + createIssueLightbox = issuesHelper.getCreateIssueLightbox(); + + issuesHelper.openNewIssueLb(); + + await createIssueLightbox.waitOpen(); + }); + + it('capture screen', function() { + utils.common.takeScreenshot('issues', 'create-issue'); + }); + + it('fill form', async function() { + // subject + createIssueLightbox.subject().sendKeys('subject'); + + // tags + await createIssueLightbox.tags().sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + await createIssueLightbox.tags().sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + utils.common.takeScreenshot('issues', 'create-issue-filled'); + }); + + it('send form', async function() { + createIssueLightbox.submit(); + + await createIssueLightbox.waitClose(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + }); + + describe('bulk create Issue', function() { + let createIssueLightbox = null; + + before(async function() { + issuesHelper.openBulk(); + + createIssueLightbox = issuesHelper.getBulkCreateLightbox(); + + createIssueLightbox.waitOpen(); + }); + + it('fill form', function() { + createIssueLightbox.textarea().sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + createIssueLightbox.textarea().sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + }); + + it('send form', async function() { + createIssueLightbox.submit(); + + await createIssueLightbox.waitClose(); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + }); + + it('change order', async function() { + let table = issuesHelper.getTable(); + + // test every column order + for(let i = 0; i < 7; i++) { + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.clickColumn(i); + await htmlChanges(); + + htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.clickColumn(i); + await htmlChanges(); + } + }); + + it('assignto to', async function() { + let assignToLightbox = commonHelper.assignToLightbox(); + + issuesHelper.openAssignTo(0); + + await assignToLightbox.waitOpen(); + + let newUserName = await assignToLightbox.getName(2); + + assignToLightbox.select(2); + + await assignToLightbox.waitClose(); + + let issueUserName = await issuesHelper.getAssignTo(0); + + expect(issueUserName).to.be.equal(newUserName); + }); + + describe('filters', function() { + it('by ref', async function() { + let table = issuesHelper.getTable(); + let issues = issuesHelper.getIssues(); + let issue = issues.get(0); + issue = await issuesHelper.parseIssue(issue); + let filterInput = issuesHelper.getFilterInput(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + await filterInput.sendKeys(issue.ref); + await htmlChanges(); + + let newIssuesCount = await issues.count(); + + expect(newIssuesCount).to.be.equal(1); + + htmlChanges = await utils.common.outerHtmlChanges(table); + await utils.common.clear(filterInput); + await htmlChanges(); + }); + + it('by subject', async function() { + let table = issuesHelper.getTable(); + let issues = issuesHelper.getIssues(); + let issue = issues.get(0); + issue = await issuesHelper.parseIssue(issue); + let filterInput = issuesHelper.getFilterInput(); + + let oldIssuesCount = await $$('.row.table-main').count(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + await filterInput.sendKeys(issue.subject); + await htmlChanges(); + + let newIssuesCount = await issues.count(); + + expect(newIssuesCount).not.to.be.equal(oldIssuesCount); + expect(newIssuesCount).to.be.above(0); + + htmlChanges = await utils.common.outerHtmlChanges(table); + await utils.common.clear(filterInput); + await htmlChanges(); + }); + + it('by type', async function() { + let table = issuesHelper.getTable(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.filtersCats().get(0).$('a').click(); + issuesHelper.selectFilter(0); + await htmlChanges(); + + issuesHelper.backToFilters(); + + await issuesHelper.removeFilters(); + }); + + it('by status', async function() { + let table = issuesHelper.getTable(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.filtersCats().get(1).$('a').click(); + issuesHelper.selectFilter(0); + await htmlChanges(); + + issuesHelper.backToFilters(); + + await issuesHelper.removeFilters(); + }); + + it('by severity', async function() { + let table = issuesHelper.getTable(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.filtersCats().get(2).$('a').click(); + issuesHelper.selectFilter(0); + await htmlChanges(); + + issuesHelper.backToFilters(); + + await issuesHelper.removeFilters(); + }); + + it('by priorities', async function() { + let table = issuesHelper.getTable(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.filtersCats().get(3).$('a').click(); + issuesHelper.selectFilter(0); + await htmlChanges(); + + issuesHelper.backToFilters(); + + await issuesHelper.removeFilters(); + }); + + it('by tags', async function() { + let table = issuesHelper.getTable(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.filtersCats().get(4).$('a').click(); + issuesHelper.selectFilter(1); + await htmlChanges(); + + issuesHelper.backToFilters(); + + await issuesHelper.removeFilters(); + }); + + it('by assigned to', async function() { + let table = issuesHelper.getTable(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.filtersCats().get(5).$('a').click(); + issuesHelper.selectFilter(0); + await htmlChanges(); + + issuesHelper.backToFilters(); + + await issuesHelper.removeFilters(); + }); + + it('by created by', async function() { + let table = issuesHelper.getTable(); + + let htmlChanges = await utils.common.outerHtmlChanges(table); + issuesHelper.filtersCats().get(6).$('a').click(); + issuesHelper.selectFilter(0); + await htmlChanges(); + + issuesHelper.backToFilters(); + + await issuesHelper.removeFilters(); + }); + + it('empty', async function() { + let table = issuesHelper.getTable(); + let htmlChanges = await utils.common.outerHtmlChanges(table); + + let filterInput = issuesHelper.getFilterInput(); + + await filterInput.sendKeys(new Date().getTime()); + + await htmlChanges(); + + let newIssuesCount = await issuesHelper.getIssues().count(); + + expect(newIssuesCount).to.be.equal(0); + + await utils.common.takeScreenshot('issues', 'empty-issues'); + await utils.common.clear(filterInput); + }); + + it('save custom filter', async function() { + issuesHelper.filtersCats().get(1).$('a').click(); + issuesHelper.selectFilter(0); + + await browser.waitForAngular(); + + await issuesHelper.saveFilter('custom'); + + expect(issuesHelper.getCustomFilters().count()).to.be.eventually.equal(1); + + await issuesHelper.removeFilters(); + issuesHelper.backToFilters(); + }); + + it('apply custom filter', async function() { + let table = issuesHelper.getTable(); + let htmlChanges = await utils.common.outerHtmlChanges(table); + + issuesHelper.filtersCats().get(7).$('a').click(); + + issuesHelper.selectFilter(0); + + await htmlChanges(); + + await issuesHelper.removeFilters(); + }); + + it('remove custom filter', async function() { + await issuesHelper.removeCustomFilters(); + + expect(issuesHelper.getCustomFilters().count()).to.be.eventually.equal(0); + }); + }); +}); diff --git a/e2e/full/kanban.e2e.js b/e2e/full/kanban.e2e.js new file mode 100644 index 00000000..2c7caf3d --- /dev/null +++ b/e2e/full/kanban.e2e.js @@ -0,0 +1,296 @@ +var utils = require('../utils'); +var kanbanHelper = require('../helpers').kanban; +var backlogHelper = require('../helpers').backlog; +var commonHelper = require('../helpers').common; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('kanban', function() { + before(async function() { + browser.get(browser.params.glob.host + 'project/project-0/kanban'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('kanban', 'kanban'); + }); + + describe('create us', function() { + let createUSLightbox = null; + let formFields = {}; + + before(async function() { + kanbanHelper.openNewUsLb(0); + + createUSLightbox = backlogHelper.getCreateEditUsLightbox(); + + await createUSLightbox.waitOpen(); + }); + + it('capture screen', function() { + utils.common.takeScreenshot('kanban', 'create-us'); + }); + + it('fill form', async function() { + let date = Date.now(); + + formFields.subject = 'test subject' + date; + formFields.description = 'test description' + date; + + // subject + createUSLightbox.subject().sendKeys(formFields.subject); + + // roles + createUSLightbox.setRole(1, 3); + createUSLightbox.setRole(2, 3); + createUSLightbox.setRole(3, 3); + createUSLightbox.setRole(4, 3); + + let totalPoints = await createUSLightbox.getRolePoints(); + + expect(totalPoints).to.be.equal('4'); + + // tags + createUSLightbox.tags().sendKeys('www'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + createUSLightbox.tags().sendKeys('xxx'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + // description + createUSLightbox.description().sendKeys(formFields.description); + + //settings + createUSLightbox.settings(1).click(); + }); + + it('send form', async function() { + createUSLightbox.submit(); + + await utils.lightbox.close(createUSLightbox.el); + + let ussTitles = await kanbanHelper.getColumnUssTitles(0); + + let findSubject = ussTitles.indexOf(formFields.subject) !== 1; + + expect(findSubject).to.be.true; + }); + }); + + + describe('edit us', function() { + let createUSLightbox = null; + let formFields = {}; + + before(async function() { + kanbanHelper.editUs(0, 0); + + createUSLightbox = backlogHelper.getCreateEditUsLightbox(); + + await createUSLightbox.waitOpen(); + }); + + it('capture screen', function() { + utils.common.takeScreenshot('kanban', 'edit-us'); + }); + + it('fill form', async function() { + let date = Date.now(); + + formFields.subject = 'test subject' + date; + formFields.description = 'test description' + date; + + // subject + let subject = createUSLightbox.subject(); + + await subject.clear(); + + subject.sendKeys(formFields.subject); + + // roles + createUSLightbox.setRole(1, 3); + createUSLightbox.setRole(2, 3); + createUSLightbox.setRole(3, 3); + createUSLightbox.setRole(4, 3); + + let totalPoints = await createUSLightbox.getRolePoints(); + + expect(totalPoints).to.be.equal('4'); + + // tags + createUSLightbox.tags().sendKeys('www'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + createUSLightbox.tags().sendKeys('xxx'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + // description + createUSLightbox.description().sendKeys(formFields.description); + + //settings + createUSLightbox.settings(1).click(); + }); + + it('send form', async function() { + createUSLightbox.submit(); + + await utils.lightbox.close(createUSLightbox.el); + + let ussTitles = await kanbanHelper.getColumnUssTitles(0); + + let findSubject = ussTitles.indexOf(formFields.subject) !== -1; + + expect(findSubject).to.be.true; + }); + }); + + describe('bulk create', function() { + let createUSLightbox; + + before(async function() { + kanbanHelper.openBulkUsLb(0); + + createUSLightbox = backlogHelper.getBulkCreateLightbox(); + + await createUSLightbox.waitOpen(); + }); + + it('fill form', function() { + createUSLightbox.textarea().sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + createUSLightbox.textarea().sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + }); + + it('send form', async function() { + let ussCount = await kanbanHelper.getBoxUss(0).count(); + + createUSLightbox.submit(); + + await utils.lightbox.close(createUSLightbox.el); + + let newUssCount = await kanbanHelper.getBoxUss(0).count(); + + expect(newUssCount).to.be.equal(ussCount + 2); + }); + }); + + describe('folds', function() { + it('fold column', async function() { + kanbanHelper.foldColumn(0); + + utils.common.takeScreenshot('kanban', 'fold-column'); + + let foldedColumns = await $$('.vfold.task-column').count(); + + expect(foldedColumns).to.be.equal(1); + }); + + it('unfold column', async function() { + kanbanHelper.unFoldColumn(0); + + let foldedColumns = await $$('.vfold.task-column').count(); + + expect(foldedColumns).to.be.equal(0); + }); + + it('fold cars', async function() { + kanbanHelper.foldCards(0); + + utils.common.takeScreenshot('kanban', 'fold-cards'); + + let minimized = await $$('.kanban-task-minimized').count(); + + expect(minimized).to.be.above(1); + }); + + it('unfold cars', async function() { + kanbanHelper.unFoldCards(0); + + let minimized = await $$('.kanban-task-minimized').count(); + + expect(minimized).to.be.equal(0); + }); + }); + + it('move us between columns', async function() { + let initOriginUsCount = await kanbanHelper.getBoxUss(0).count(); + let initDestinationUsCount = await kanbanHelper.getBoxUss(1).count(); + + let usOrigin = kanbanHelper.getBoxUss(0).first(); + let destination = kanbanHelper.getColumns().get(1); + + await utils.common.drag(usOrigin, destination); + + browser.waitForAngular(); + + let originUsCount = await kanbanHelper.getBoxUss(0).count(); + let destinationUsCount = await kanbanHelper.getBoxUss(1).count(); + + expect(originUsCount).to.be.equal(initOriginUsCount - 1); + expect(destinationUsCount).to.be.equal(initDestinationUsCount + 1); + }); + + describe('archive', function() { + utils.common.browserSkip('firefox', 'move to archive', async function() { + let initOriginUsCount = await kanbanHelper.getBoxUss(3).count(); + + let usOrigin = kanbanHelper.getBoxUss(3).first(); + let destination = kanbanHelper.getColumns().last(); + + await kanbanHelper.scrollRight(); + + await utils.common.drag(usOrigin, destination); + + browser.waitForAngular(); + + let originUsCount = await kanbanHelper.getBoxUss(3).count(); + + utils.common.takeScreenshot('kanban', 'archive'); + + expect(originUsCount).to.be.equal(initOriginUsCount - 1); + }); + + utils.common.browserSkip('firefox', 'show archive', async function() { + $('.icon-open-eye').click(); + + await kanbanHelper.scrollRight(); + + utils.common.takeScreenshot('kanban', 'archive-open'); + + let usCount = await kanbanHelper.getBoxUss(5).count(); + + expect(usCount).to.be.above(0); + }); + + utils.common.browserSkip('firefox', 'close archive', async function() { + $('.icon-closed-eye').click(); + + let usCount = await kanbanHelper.getBoxUss(5).count(); + + expect(usCount).to.be.equal(0); + }); + }); + + it('edit assigned to', async function() { + await kanbanHelper.watchersLinks().first().click(); + + let lightbox = commonHelper.assignToLightbox(); + + await lightbox.waitOpen(); + + let assgnedToName = await lightbox.getName(0); + + lightbox.selectFirst(); + + await lightbox.waitClose(); + + let usAssignedTo = await kanbanHelper.getBoxUss(0).get(0).$('.task-assigned').getText(); + + expect(assgnedToName).to.be.equal(usAssignedTo); + }); +}); diff --git a/e2e/full/project-home.e2e.js b/e2e/full/project-home.e2e.js new file mode 100644 index 00000000..dcaa0ec1 --- /dev/null +++ b/e2e/full/project-home.e2e.js @@ -0,0 +1,129 @@ +var utils = require('../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('project home', function() { + beforeEach(async function() { + browser.get(browser.params.glob.host + 'project/project-1/'); + await utils.common.waitLoader(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("project", "home"); + }); + + it('go to project', async function() { + await utils.common.goToFirstProject(); + }); +/* + it('timeline filled', function() { + expect($$('div[tg-user-timeline-item]').count()).to.be.eventually.above(0); + }); + + it('timeline pagination', async function() { + let startTotal = await $$('div[tg-user-timeline-item]').count(); + + await browser.executeScript('window.scrollTo(0,document.body.scrollHeight)'); + await browser.waitForAngular(); + + let endTotal = await $$('div[tg-user-timeline-item]').count(); + + let hasMoreItems = startTotal < endTotal; + + expect(hasMoreItems).to.be.equal(true); + }); + + it('team filled', function() { + expect($$('ul.involved-team a').count()).to.be.eventually.above(0); + }); +*/ + it('unlike', async function() { + let link = $('tg-like-project-button a'); + let likesCounterOld = parseInt(await link.$('.track-button-counter').getText(), 10); + + link.click(); + + await browser.waitForAngular(); + + let likeActive = utils.common.hasClass(link, 'active'); + let likesCounter = parseInt(await link.$('.track-button-counter').getText(), 10); + + expect(likeActive).to.be.eventually.false; + expect(likesCounter).to.be.equal(likesCounterOld - 1); + }); + + it('like', async function() { + let link = $('tg-like-project-button a'); + let likesCounterOld = parseInt(await link.$('.track-button-counter').getText(), 10); + + link.click(); + + await browser.waitForAngular(); + await utils.common.takeScreenshot("project", "home-like"); + + let likeActive = utils.common.hasClass(link, 'active'); + let likesCounter = parseInt(await link.$('.track-button-counter').getText(), 10); + + expect(likeActive).to.be.eventually.true; + expect(likesCounter).to.be.equal(likesCounterOld + 1); + }); + + it('unwatch', async function() { + let link = $('tg-watch-project-button > a'); + let watchOptions = $('tg-watch-project-button .watch-options'); + let watchCounterOld = parseInt(await link.$('.track-button-counter').getText(), 10); + + link.click(); + + await browser.waitForAngular(); + + await browser.wait(async () => { + return !await utils.common.hasClass(watchOptions, 'hidden'); + }, 4000); + + watchOptions.$$('a').last().click(); + + await browser.wait(async () => { + return await utils.common.hasClass(watchOptions, 'hidden'); + }, 4000); + + let watchActive = utils.common.hasClass(link, 'active'); + let watchCounter = parseInt(await link.$('.track-button-counter').getText(), 10); + + expect(watchActive).to.be.eventually.false; + expect(watchCounter).to.be.equal(watchCounterOld - 1); + }); + + it('watch', async function() { + let link = $('tg-watch-project-button > a'); + let watchOptions = $('tg-watch-project-button .watch-options'); + let watchCounterOld = parseInt(await link.$('.track-button-counter').getText(), 10); + + link.click(); + + await browser.waitForAngular(); + + await browser.wait(async () => { + return !await utils.common.hasClass(watchOptions, 'hidden'); + }, 4000); + + watchOptions.$$('a').first().click(); + + await browser.wait(async () => { + return await utils.common.hasClass(watchOptions, 'hidden'); + }, 4000); + + let watchActive = utils.common.hasClass(link, 'active'); + let watchCounter = parseInt(await link.$('.track-button-counter').getText(), 10); + + await utils.common.takeScreenshot("project", "home-watch"); + + expect(watchActive).to.be.eventually.true; + expect(watchCounter).to.be.equal(watchCounterOld + 1); + }); + +}); diff --git a/e2e/full/search.e2e.js b/e2e/full/search.e2e.js new file mode 100644 index 00000000..7e3b86fb --- /dev/null +++ b/e2e/full/search.e2e.js @@ -0,0 +1,103 @@ +var utils = require('../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('search page', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/'); + + await utils.common.waitLoader(); + }); + + it('lightbox', async function() { + $('#nav-search a').click(); + + let searchLb = $('div[tg-search-box]'); + + await utils.lightbox.open(searchLb); + + utils.common.takeScreenshot('search', 'search-lb'); + + await $('#search-text').sendKeys('create'); + + searchLb.$('button[type="submit"]').click(); + + await browser.waitForAngular(); + + let currentUrl = await browser.getCurrentUrl(); + + utils.common.takeScreenshot('search', 'usertories'); + + expect(currentUrl).to.be.equal(browser.params.glob.host + 'project/project-0/search?text=create'); + }); + + describe('tabs', function() { + it('issues tab', async function() { + let option = $$('.search-filter li').get(1).$('a'); + option.click(); + + utils.common.takeScreenshot('search', 'issues'); + + let active = await utils.common.hasClass(option, 'active'); + + expect(active).to.be.true; + }); + + it('tasks tab', async function() { + let option = $$('.search-filter li').get(2).$('a'); + option.click(); + + utils.common.takeScreenshot('search', 'tasks'); + + let active = await utils.common.hasClass(option, 'active'); + + expect(active).to.be.true; + }); + + it('wiki tab', async function() { + let option = $$('.search-filter li').get(3).$('a'); + option.click(); + + utils.common.takeScreenshot('search', 'wiki'); + + let active = await utils.common.hasClass(option, 'active'); + + expect(active).to.be.true; + }); + + it('userstories tab', async function() { + let option = $$('.search-filter li').get(0).$('a'); + option.click(); + + let active = await utils.common.hasClass(option, 'active'); + + expect(active).to.be.true; + }); + }); + + describe('new search', function() { + it('change current tab content on typing in the right column', async function() { + let searchTerm = element(by.model('searchTerm')); + await searchTerm.clear(); + + let text = await $$('.table-main').get(0).$('a').getText(); + + let htmlChanges = await utils.common.outerHtmlChanges('.search-result-table-body'); + + let initialCount = await $$('.table-main').count(); + + await searchTerm.sendKeys(text); + + await htmlChanges(); + + let count = await $$('.table-main').count(); + + expect(count).to.below(initialCount); + expect(count).to.above(0); + }); + }); +}); diff --git a/e2e/full/tasks/task-detail.e2e.js b/e2e/full/tasks/task-detail.e2e.js new file mode 100644 index 00000000..01766da0 --- /dev/null +++ b/e2e/full/tasks/task-detail.e2e.js @@ -0,0 +1,75 @@ +var utils = require('../../utils'); +var taskDetailHelper = require('../../helpers').taskDetail; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('Task detail', function(){ + let taskUrl = ''; + + before(async function(){ + await utils.nav + .init() + .project('Project Example 0') + .backlog() + .taskboard(0) + .task(0) + .go(); + + taskUrl = await browser.driver.getCurrentUrl(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("tasks", "detail"); + }); + + it('title edition', utils.detail.titleTesting); + + it('tags edition', utils.detail.tagsTesting); + + it('description edition', utils.detail.descriptionTesting); + + it('status edition', utils.detail.statusTesting); + + it('assigned to edition', utils.detail.assignedToTesting); + + it('watchers edition', utils.detail.watchersTesting); + + it('iocaine edition', async function() { + // Toggle iocaine status + let iocaineHelper = taskDetailHelper.iocaine(); + let isIocaine = await iocaineHelper.isIocaine() + iocaineHelper.togleIocaineStatus(); + let newIsIocaine = await iocaineHelper.isIocaine() + expect(newIsIocaine).to.be.not.equal(isIocaine); + + // Toggle again + iocaineHelper.togleIocaineStatus(); + newIsIocaine = await iocaineHelper.isIocaine() + expect(newIsIocaine).to.be.equal(isIocaine); + }); + + it('history', utils.detail.historyTesting); + + it('block', utils.detail.blockTesting); + + it('attachments', utils.detail.attachmentTesting); + + describe('custom-fields', utils.detail.customFields.bind(this, 1)); + + it('screenshot', async function() { + await utils.common.takeScreenshot("tasks", "detail updated"); + }); + + describe('delete & redirect', function() { + it('delete', utils.detail.deleteTesting); + + it('redirected', async function (){ + let url = await browser.getCurrentUrl(); + expect(url).not.to.be.equal(taskUrl); + }); + }); +}); diff --git a/e2e/full/tasks/taskboard.e2e.js b/e2e/full/tasks/taskboard.e2e.js new file mode 100644 index 00000000..ceac074a --- /dev/null +++ b/e2e/full/tasks/taskboard.e2e.js @@ -0,0 +1,299 @@ +var utils = require('../../utils'); +var backlogHelper = require('../../helpers').backlog; +var taskboardHelper = require('../../helpers').taskboard; +var commonHelper = require('../../helpers').common; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('taskboard', function() { + before(async function() { + await utils.nav + .init() + .project('Project Example 1') + .backlog() + .taskboard(0) + .go(); + + utils.common.takeScreenshot('taskboard', 'taskboard'); + }); + + describe('create task', function() { + let createTaskLightbox = null; + let formFields = {}; + + before(async function() { + taskboardHelper.openNewTaskLb(0); + + createTaskLightbox = taskboardHelper.getCreateTask(); + + await createTaskLightbox.waitOpen(); + }); + + it('capture screen', function() { + utils.common.takeScreenshot('taskboard', 'create-task'); + }); + + it('fill form', async function() { + let date = Date.now(); + formFields.subject = 'test subject' + date; + formFields.description = 'test description' + date; + formFields.blockedNote = 'blocked note'; + + createTaskLightbox.subject().sendKeys(formFields.subject); + createTaskLightbox.description().sendKeys(formFields.description); + + createTaskLightbox.tags().sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + createTaskLightbox.tags().sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + await createTaskLightbox.blocked().click(); + await createTaskLightbox.blockedNote().sendKeys(formFields.blockedNote); + + utils.common.takeScreenshot('taskboard', 'create-task-filled'); + }); + + it('send form', async function() { + createTaskLightbox.submit(); + + await utils.lightbox.close(createTaskLightbox.el); + + let tasks = taskboardHelper.getBoxTasks(0, 0); + + let tasksSubject = await $$('.task-name').getText(); + + let findSubject = tasksSubject.indexOf(formFields.subject) !== -1; + + expect(findSubject).to.be.true; + }); + }); + + describe('edit task', function() { + let createTaskLightbox = null; + let formFields = {}; + + before(async function() { + taskboardHelper.editTask(0, 0, 0); + + createTaskLightbox = taskboardHelper.getCreateTask(); + + await createTaskLightbox.waitOpen(); + }); + + it('capture screen', function() { + utils.common.takeScreenshot('taskboard', 'edit-task'); + }); + + it('fill form', async function() { + let date = Date.now(); + formFields.subject = 'test subject' + date; + formFields.description = 'test description' + date; + formFields.blockedNote = 'blocked note'; + + createTaskLightbox.subject().sendKeys(formFields.subject); + await createTaskLightbox.description().sendKeys(formFields.description); + + await utils.common.takeScreenshot('taskboard', 'edit-task-filled'); + + // send form fail when all tests are launched + await browser.sleep(1000); + }); + + it('send form', async function() { + createTaskLightbox.submit(); + + await utils.lightbox.close(createTaskLightbox.el); + + let tasks = taskboardHelper.getBoxTasks(0, 0); + + let tasksSubject = await $$('.task-name').getText(); + + let findSubject = tasksSubject.indexOf(formFields.subject) !== 1; + + expect(findSubject).to.be.true; + }); + }); + + describe('bulk create', function() { + let bulkCreateTaskLightbox; + + before(async function() { + taskboardHelper.openBulkTaskLb(0); + + bulkCreateTaskLightbox = taskboardHelper.getBulkCreateTask(); + + await bulkCreateTaskLightbox.waitOpen(); + }); + + it('fill form', function() { + bulkCreateTaskLightbox.textarea().sendKeys('aaa'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + + bulkCreateTaskLightbox.textarea().sendKeys('bbb'); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + }); + + it('send form', async function() { + let taskCount = await taskboardHelper.getBoxTasks(0, 0).count(); + + bulkCreateTaskLightbox.submit(); + + await utils.lightbox.close(bulkCreateTaskLightbox.el); + + let newTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); + + expect(newTaskCount).to.be.equal(taskCount + 2); + }); + }); + + describe('folds', function() { + it('fold row', async function() { + taskboardHelper.foldRow(0); + + utils.common.takeScreenshot('taskboard', 'fold-row'); + + let rowsFold = await $$('.row-fold').count(); + + expect(rowsFold).to.be.equal(1); + }); + + it('unfold row', async function() { + taskboardHelper.unFoldRow(0); + + let rowsFold = await $$('.row-fold').count(); + + expect(rowsFold).to.be.equal(0); + }); + + it('fold column', async function() { + taskboardHelper.foldColumn(0); + + utils.common.takeScreenshot('taskboard', 'fold-column'); + + let columnFold = await $$('.column-fold').count(); + + expect(columnFold).to.be.above(1); + }); + + it('unfold column', async function() { + taskboardHelper.unFoldColumn(0); + + let columnFold = await $$('.column-fold').count(); + + expect(columnFold).to.be.equal(0); + }); + + it('fold row and column', async function() { + taskboardHelper.foldRow(0); + taskboardHelper.foldColumn(0); + + utils.common.takeScreenshot('taskboard', 'fold-column-row'); + + let rowsFold = await $$('.row-fold').count(); + let columnFold = await $$('.column-fold').count(); + + expect(rowsFold).to.be.equal(1); + expect(columnFold).to.be.above(1); + }); + + it('unfold row and column', async function() { + taskboardHelper.unFoldRow(0); + taskboardHelper.unFoldColumn(0); + + let rowsFold = await $$('.row-fold').count(); + let columnFold = await $$('.column-fold').count(); + + expect(rowsFold).to.be.equal(0); + expect(columnFold).to.be.equal(0); + }); + }); + + describe('move tasks', function() { + it('move task between statuses', async function() { + let initOriginTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); + let initDestinationTaskCount = await taskboardHelper.getBoxTasks(0, 1).count(); + + let taskOrigin = taskboardHelper.getBoxTasks(0, 0).first(); + let destination = taskboardHelper.getBox(0, 1); + + await utils.common.drag(taskOrigin, destination); + + browser.waitForAngular(); + + let originTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); + let destinationTaskCount = await taskboardHelper.getBoxTasks(0, 1).count(); + + expect(originTaskCount).to.be.equal(initOriginTaskCount - 1); + expect(destinationTaskCount).to.be.equal(initDestinationTaskCount + 1); + }); + + // jquery ui drag bug + it.skip('move task between US\s', async function() { + let initOriginTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); + let initDestinationTaskCount = await taskboardHelper.getBoxTasks(1, 1).count(); + + let taskOrigin = taskboardHelper.getBoxTasks(0, 0).first(); + let destination = taskboardHelper.getBox(1, 0); + + await utils.common.drag(taskOrigin, destination); + + browser.waitForAngular(); + + let originTaskCount = await taskboardHelper.getBoxTasks(0, 0).count(); + let destinationTaskCount = await taskboardHelper.getBoxTasks(1, 1).count(); + + expect(originTaskCount).to.be.equal(initOriginTaskCount - 1); + expect(destinationTaskCount).to.be.equal(initDestinationTaskCount + 1); + }); + }); + + + it('Change task assigned to', async function(){ + await taskboardHelper.watchersLinks().first().click(); + + let lightbox = commonHelper.assignToLightbox(); + + await lightbox.waitOpen(); + + let assgnedToName = await lightbox.getName(0); + + lightbox.selectFirst(); + + await lightbox.waitClose(); + + let usAssignedTo = await taskboardHelper.getBoxTasks(0, 0).get(0).$('.task-assigned').getText(); + + expect(assgnedToName).to.be.equal(usAssignedTo); + }); + + describe('Graph', function(){ + let graph = $('.graphics-container'); + + it('open', async function() { + taskboardHelper.toggleGraph(); + + await utils.common.waitTransitionTime(graph); + + utils.common.takeScreenshot('taskboard', 'grap-open'); + + let open = await utils.common.hasClass(graph, 'open'); + + expect(open).to.be.true; + }); + + it('close', async function() { + taskboardHelper.toggleGraph(); + + await utils.common.waitTransitionTime(graph); + + let open = await utils.common.hasClass(graph, 'open'); + + expect(open).to.be.false; + }); + }); +}); diff --git a/e2e/full/team.e2e.js b/e2e/full/team.e2e.js new file mode 100644 index 00000000..64e12039 --- /dev/null +++ b/e2e/full/team.e2e.js @@ -0,0 +1,57 @@ +var utils = require('../utils'); +var teamHelper = require('../helpers').team; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('leaving project', function(){ + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-4/team'); + await utils.common.waitLoader(); + }); + + it('leave project', async function(){ + teamHelper.team().leave(); + await utils.lightbox.confirm.ok(); + await utils.common.takeScreenshot("team", "after-leaving"); + }); +}); + +describe('team', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-5/team'); + await utils.common.waitLoader(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("team", "team"); + }); + + it('team filled', async function() { + let total = await teamHelper.team().count(); + expect(total).to.be.equal(10); + }); + + it('search username', async function() { + let firstMemberName = await teamHelper.team().firstMember().getText(); + teamHelper.filters().searchText(firstMemberName); + let total = await teamHelper.team().count(); + expect(total).to.be.equal(1); + await utils.common.takeScreenshot("team", "searching-by-name"); + }); + + it('filter role', async function(){ + teamHelper.filters().clearText(); + let total = await teamHelper.team().count(); + let firstRole = await teamHelper.team().firstRole(); + let roleName = await firstRole.getText(); + teamHelper.filters().filterByRole(roleName); + let newTotal = await teamHelper.team().count(); + expect(newTotal).to.be.below(total); + expect(newTotal).to.be.least(1); + await utils.common.takeScreenshot("team", "filtering-by-role"); + }); +}); diff --git a/e2e/full/user-profile/change-password.e2e.js b/e2e/full/user-profile/change-password.e2e.js new file mode 100644 index 00000000..301ab3e4 --- /dev/null +++ b/e2e/full/user-profile/change-password.e2e.js @@ -0,0 +1,66 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('change password', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'user-settings/user-change-password'); + await utils.common.waitLoader(); + + utils.common.takeScreenshot('edit-user-profile', 'change-password'); + }); + + beforeEach(async function() { + browser.get(browser.params.glob.host + 'user-settings/user-change-password'); + + await utils.common.waitLoader(); + }); + + it('retype different', async function() { + await $('#current-password').sendKeys('123123'); + await $('#new-password').sendKeys('123456'); + await $('#retype-password').sendKeys('000'); + + $('button[type="submit"]').click(); + + expect(utils.notifications.error.open()).to.be.eventually.equal(true); + }); + + it('incorrect current password', async function() { + await $('#current-password').sendKeys('aaaa'); + await $('#new-password').sendKeys('123456'); + await $('#retype-password').sendKeys('123456'); + + $('button[type="submit"]').click(); + + expect(utils.notifications.error.open()).to.be.eventually.equal(true); + }); + + it('change password', async function() { + await $('#current-password').sendKeys('123123'); + await $('#new-password').sendKeys('aaabbb'); + await $('#retype-password').sendKeys('aaabbb'); + + $('button[type="submit"]').click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + }); + + after(async function() { + browser.get(browser.params.glob.host + 'user-settings/user-change-password'); + await utils.common.waitLoader(); + + //restore + await $('#current-password').sendKeys('aaabbb'); + await $('#new-password').sendKeys('123123'); + await $('#retype-password').sendKeys('123123'); + + $('button[type="submit"]').click(); + + await browser.waitForAngular(); + }) +}); diff --git a/e2e/full/user-profile/edit-user-profile.e2e.js b/e2e/full/user-profile/edit-user-profile.e2e.js new file mode 100644 index 00000000..3c22a3c2 --- /dev/null +++ b/e2e/full/user-profile/edit-user-profile.e2e.js @@ -0,0 +1,100 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('edit user profile', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'user-settings/user-profile'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('edit-user-profile', 'edit-user-profile'); + }); + + it('edit fullname', async function() { + $('#full-name').clear().sendKeys('admin-' + Date.now()); + + $('button[type="submit"]').click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + + // debounce :( + await browser.sleep(2000); + }); + + it('update email', async function() { + let email = $('#email'); + + await email.clear().sendKeys('admin+1@admin.com'); + + $('button[type="submit"]').click(); + + let lb = $('.lightbox-generic-success'); + + await utils.lightbox.open(lb); + + lb.$('.button-green').click(); + + await utils.lightbox.close(lb); + + // debounce :( + await browser.sleep(2000); + }); + + it('edit lenguage', async function() { + // english + $('#lang option:nth-child(4)').click(); + $('button[type="submit"]').click(); + + await utils.notifications.success.open(); + + //debounce + browser.sleep(2000); + + let pageTitle = await $('h1 span').getText(); + let lang = $('#lang option:nth-child(2)').click(); + + $('button[type="submit"]').click(); + + await utils.notifications.success.open(); + + let newPageTitle = await $('h1 span').getText(); + + expect(newPageTitle).to.be.not.equal(pageTitle); + + //debounce + browser.sleep(2000); + + // revert english + $('#lang option:nth-child(4)').click(); + $('button[type="submit"]').click(); + + await utils.notifications.success.open(); + + //debounce + browser.sleep(2000); + }); + + it('edit avatar', async function() { + let inputFile = $('#avatar-field'); + + let imageContainer = $('.image-container'); + + let htmlChanges = await utils.common.outerHtmlChanges(imageContainer); + var fileToUpload = utils.common.uploadImagePath(); + + await utils.common.uploadFile(inputFile, fileToUpload); + + await htmlChanges(); + + let avatar = imageContainer.$('.avatar'); + + let src = await avatar.getAttribute('src'); + + expect(src).to.contains('upload-image-test.png'); + }); +}); diff --git a/e2e/full/user-profile/email-notification.e2e.js b/e2e/full/user-profile/email-notification.e2e.js new file mode 100644 index 00000000..d97620db --- /dev/null +++ b/e2e/full/user-profile/email-notification.e2e.js @@ -0,0 +1,47 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('email notification', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'user-settings/mail-notifications'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('edit-user-profile', 'mail-notifications'); + }); + + it('change project notification to all', async function() { + let row = $$('.policy-table-row').get(1); + + row.$$('label').get(0).click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + + await utils.notifications.success.close(); + }); + + it('change project notification to no', async function() { + let row = $$('.policy-table-row').get(1); + + row.$$('label').get(2).click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + + await utils.notifications.success.close(); + }); + + it('change project notification to only', async function() { + let row = $$('.policy-table-row').get(1); + + row.$$('label').get(1).click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + + await utils.notifications.success.close(); + }); +}); diff --git a/e2e/full/user-profile/feedback.e2e.js b/e2e/full/user-profile/feedback.e2e.js new file mode 100644 index 00000000..0d71722c --- /dev/null +++ b/e2e/full/user-profile/feedback.e2e.js @@ -0,0 +1,31 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('feedback', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'user-settings/mail-notifications'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('edit-user-profile', 'mail-notifications'); + }); + + it('send feedback', async function() { + await utils.common.topMenuOption(4); + + let feedbackLightbox = $('div[tg-lb-feedback]'); + + await utils.lightbox.open(feedbackLightbox); + + await feedbackLightbox.$('textarea').sendKeys('test test test'); + + feedbackLightbox.$('button[type=submit]').click(); + + expect(utils.notifications.success.open()).to.be.eventually.equal(true); + }); +}); diff --git a/e2e/full/user-profile/user-profile-activity.e2e.js b/e2e/full/user-profile/user-profile-activity.e2e.js new file mode 100644 index 00000000..3a21b4fa --- /dev/null +++ b/e2e/full/user-profile/user-profile-activity.e2e.js @@ -0,0 +1,83 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('user profile - activity', function() { + describe('current user', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'profile'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('user-profile', 'current-user-activity'); + }); + + it('activity tab - pagination', async function() { + let startTotal = await $$('div[tg-user-timeline-item]').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await browser.executeScript('window.scrollTo(0,document.body.scrollHeight)'); + await htmlChanges(); + + let endTotal = await $$('div[tg-user-timeline-item]').count(); + + let hasMoreItems = startTotal < endTotal; + + expect(hasMoreItems).to.be.equal(true); + }); + + it('conctacts tab', async function() { + $$('.tab').get(4).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'current-user-contacts'); + + let contactsCount = await $$('.list-itemtype-user').count(); + + expect(contactsCount).to.be.above(0); + }); + + utils.common.browserSkip('internet explorer', 'edit profile hover', async function() { + let userImage = $('.profile-image-wrapper'); + + await browser.actions().mouseMove(userImage).perform(); + + let profileEdition = userImage.$('.profile-edition'); + + await utils.common.waitTransitionTime(profileEdition); + + utils.common.takeScreenshot('user-profile', 'image-hover'); + + expect(profileEdition.isDisplayed()).to.be.eventually.true; + }); + }); + + describe('other user', function() { + before(async function(){ + browser.get(browser.params.glob.host + 'profile/user7'); + + await utils.common.waitLoader(); + + utils.common.takeScreenshot('user-profile', 'other-user-activity'); + }); + + it('activity tab pagination', async function() { + let startTotal = await $$('div[tg-user-timeline-item]').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await browser.executeScript('window.scrollTo(0,document.body.scrollHeight)'); + await htmlChanges(); + + let endTotal = await $$('div[tg-user-timeline-item]').count(); + + let hasMoreItems = startTotal < endTotal; + + expect(hasMoreItems).to.be.equal(true); + }); + }); +}); diff --git a/e2e/full/user-profile/user-profile-contacts.e2e.js b/e2e/full/user-profile/user-profile-contacts.e2e.js new file mode 100644 index 00000000..d7c6e95e --- /dev/null +++ b/e2e/full/user-profile/user-profile-contacts.e2e.js @@ -0,0 +1,51 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('user profile - contacts', function() { + describe('current user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile'); + + await utils.common.waitLoader(); + + $$('.tab').get(4).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'current-user-contacts'); + }); + + it('conctacts tab', async function() { + let contactsCount = await $$('.list-itemtype-user').count(); + + expect(contactsCount).to.be.above(0); + }); + }); + + describe('other user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile/user7'); + + await utils.common.waitLoader(); + + $$('.tab').get(5).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'other-user-contacts'); + }); + + it('conctacts tab', async function() { + let contactsCount = await $$('.list-itemtype-user').count(); + + await browser.sleep(3000); + + expect(contactsCount).to.be.above(0); + }); + }); +}); diff --git a/e2e/full/user-profile/user-profile-likes.e2e.js b/e2e/full/user-profile/user-profile-likes.e2e.js new file mode 100644 index 00000000..1fec32e0 --- /dev/null +++ b/e2e/full/user-profile/user-profile-likes.e2e.js @@ -0,0 +1,90 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('user profile - likes', function() { + describe('current user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile'); + + await utils.common.waitLoader(); + + $$('.tab').get(1).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'current-user-likes'); + }); + + it('likes tab', async function() { + let likesCount = await $$('div[infinite-scroll] > div').count(); + + expect(likesCount).to.be.above(0); + }); + + it('likes tab - filter by query', async function() { + let allItems = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + $('div.searchbox > input').sendKeys('proj 2'); + await htmlChanges(); + + let filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.not.equal(filteredItems); + + htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await utils.common.clear($('div.searchbox > input')); + await htmlChanges(); + + filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.equal(filteredItems); + }); + }); + + describe('other user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile/user7'); + + await utils.common.waitLoader(); + + $$('.tab').get(2).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'other-user-likes'); + }); + + it('likes tab', async function() { + let likesCount = await $$('div[infinite-scroll] > div').count(); + + expect(likesCount).to.be.above(0); + }); + + it('likes tab - filter by query', async function() { + let allItems = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + $('div.searchbox > input').sendKeys('proj 2'); + await htmlChanges(); + + let filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.not.equal(filteredItems); + + htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await utils.common.clear($('div.searchbox > input')); + await htmlChanges(); + + filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.equal(filteredItems); + }); + + }); +}); diff --git a/e2e/full/user-profile/user-profile-projects.e2e.js b/e2e/full/user-profile/user-profile-projects.e2e.js new file mode 100644 index 00000000..c60e3009 --- /dev/null +++ b/e2e/full/user-profile/user-profile-projects.e2e.js @@ -0,0 +1,29 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('user profile - projects', function() { + describe('other user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile/user7'); + + await utils.common.waitLoader(); + + $$('.tab').get(1).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'other-user-projects'); + }); + + it('projects tab', async function() { + let projectsCount = await $$('.list-itemtype-project').count(); + + expect(projectsCount).to.be.above(0); + }); + }); +}); diff --git a/e2e/full/user-profile/user-profile-votes.e2e.js b/e2e/full/user-profile/user-profile-votes.e2e.js new file mode 100644 index 00000000..f09c0f44 --- /dev/null +++ b/e2e/full/user-profile/user-profile-votes.e2e.js @@ -0,0 +1,190 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('user profile - votes', function() { + describe('current user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile'); + + await utils.common.waitLoader(); + + $$('.tab').get(2).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'current-user-votes'); + }); + + it('votes tab', async function() { + let votesCount = await $$('div[infinite-scroll] > div').count(); + + expect(votesCount).to.be.above(0); + }); + + it('votes tab - pagination', async function() { + let startTotal = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await browser.executeScript('window.scrollTo(0,document.body.scrollHeight)'); + await htmlChanges(); + + let endTotal = await $$('div[infinite-scroll] > div').count(); + + let hasMoreItems = startTotal < endTotal; + + expect(hasMoreItems).to.be.equal(true); + }); + + it('votes tab - filter user stories', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(1).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('votes tab - filter tasks', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(2).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('votes tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(3).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('votes tab - filter by query', async function() { + let allItems = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + $('div.searchbox > input').sendKeys('test'); + await htmlChanges(); + + let filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.not.equal(filteredItems); + + htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await utils.common.clear($('div.searchbox > input')); + await htmlChanges(); + + filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.equal(filteredItems); + }); + }); + + describe('other user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile/user7'); + + await utils.common.waitLoader(); + + $$('.tab').get(3).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'other-user-votes'); + }); + + it('votes tab', async function() { + let votesCount = await $$('div[infinite-scroll] > div').count(); + + expect(votesCount).to.be.above(0); + }); + + it('votes tab - pagination', async function() { + let startTotal = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await browser.executeScript('window.scrollTo(0,document.body.scrollHeight)'); + await htmlChanges(); + + let endTotal = await $$('div[infinite-scroll] > div').count(); + + let hasMoreItems = startTotal < endTotal; + + expect(hasMoreItems).to.be.equal(true); + }); + + it('votes tab - filter user stories', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(1).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('votes tab - filter tasks', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(2).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('votes tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(3).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('votes tab - filter by query', async function() { + let allItems = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + $('div.searchbox > input').sendKeys('test'); + await htmlChanges(); + + let filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.not.equal(filteredItems); + + htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await utils.common.clear($('div.searchbox > input')); + await htmlChanges(); + + filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.equal(filteredItems); + }); + + }); +}); diff --git a/e2e/full/user-profile/user-profile-watched.e2e.js b/e2e/full/user-profile/user-profile-watched.e2e.js new file mode 100644 index 00000000..7c3ba365 --- /dev/null +++ b/e2e/full/user-profile/user-profile-watched.e2e.js @@ -0,0 +1,213 @@ +var utils = require('../../utils'); + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('user profile - watched', function() { + describe('current user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile'); + + await utils.common.waitLoader(); + + $$('.tab').get(3).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'current-user-watched'); + }); + + it('watched tab', async function() { + let watchedCount = await $$('div[infinite-scroll] > div').count(); + + expect(watchedCount).to.be.above(0); + }); + + it('watched tab - pagination', async function() { + let startTotal = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await browser.executeScript('window.scrollTo(0,document.body.scrollHeight)'); + await htmlChanges(); + + let endTotal = await $$('div[infinite-scroll] > div').count(); + + let hasMoreItems = startTotal < endTotal; + + expect(hasMoreItems).to.be.equal(true); + }); + + it('watched tab - filter projects', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(1).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter user stories', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(2).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter tasks', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(3).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(4).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter by query', async function() { + let allItems = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + $('div.searchbox > input').sendKeys('test'); + await htmlChanges(); + + let filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.not.equal(filteredItems); + + htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await utils.common.clear($('div.searchbox > input')); + await htmlChanges(); + + filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.equal(filteredItems); + }); + }); + + describe('other user', function() { + before(async function(){ + browser.get(browser.params.glob.host + '/profile/user7'); + + await utils.common.waitLoader(); + + $$('.tab').get(4).click(); + + browser.waitForAngular(); + + utils.common.takeScreenshot('user-profile', 'other-user-watched'); + }); + + it('watched tab', async function() { + let watchedCount = await $$('div[infinite-scroll] > div').count(); + + expect(watchedCount).to.be.above(0); + }); + + it('watched tab - pagination', async function() { + let startTotal = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await browser.executeScript('window.scrollTo(0,document.body.scrollHeight)'); + await htmlChanges(); + + let endTotal = await $$('div[infinite-scroll] > div').count(); + + let hasMoreItems = startTotal < endTotal; + + expect(hasMoreItems).to.be.equal(true); + }); + + it('watched tab - filter projects', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(1).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter user stories', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(2).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter tasks', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(3).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter issues', async function() { + let allItems = await $('div[infinite-scroll]').getInnerHtml(); + + await $$('div.filters > a').get(4).click(); + + await browser.waitForAngular(); + + let filteredItems = await $('div[infinite-scroll]').getInnerHtml(); + + expect(allItems).to.be.not.equal(filteredItems); + }); + + it('watched tab - filter by query', async function() { + let allItems = await $$('div[infinite-scroll] > div').count(); + + let htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + $('div.searchbox > input').sendKeys('test'); + await htmlChanges(); + + let filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.not.equal(filteredItems); + + htmlChanges = await utils.common.outerHtmlChanges('div[infinite-scroll]'); + await utils.common.clear($('div.searchbox > input')); + await htmlChanges(); + + filteredItems = await $$('div[infinite-scroll] > div').count(); + + expect(allItems).to.be.equal(filteredItems); + }); + }); +}); diff --git a/e2e/full/user-stories/user-story-detail.e2e.js b/e2e/full/user-stories/user-story-detail.e2e.js new file mode 100644 index 00000000..1bdd46b6 --- /dev/null +++ b/e2e/full/user-stories/user-story-detail.e2e.js @@ -0,0 +1,122 @@ +var utils = require('../../utils'); +var usDetailHelper = require('../../helpers').usDetail; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('User story detail', function(){ + let usUrl = ''; + + before(async function(){ + await utils.nav + .init() + .project('Project Example 0') + .backlog() + .us(0) + .go(); + + usUrl = await browser.getCurrentUrl(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("user-stories", "detail"); + }); + + it('title edition', utils.detail.titleTesting); + + it('tags edition', utils.detail.tagsTesting); + + it('description edition', utils.detail.descriptionTesting); + + it('status edition', utils.detail.statusTesting); + + it('assigned to edition', utils.detail.assignedToTesting); + + it('team requirement edition', async function() { + let requirementHelper = usDetailHelper.teamRequirement(); + let isRequired = await requirementHelper.isRequired(); + + // Toggle + requirementHelper.toggleStatus(); + let newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.not.equal(newIsRequired); + + // Toggle again + requirementHelper.toggleStatus(); + newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.equal(newIsRequired); + }); + + it('client requirement edition', async function() { + let requirementHelper = usDetailHelper.clientRequirement(); + let isRequired = await requirementHelper.isRequired(); + + // Toggle + requirementHelper.toggleStatus(); + let newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.not.equal(newIsRequired); + + // Toggle again + requirementHelper.toggleStatus(); + newIsRequired = await requirementHelper.isRequired(); + expect(isRequired).to.be.equal(newIsRequired); + }); + + it('watchers edition', utils.detail.watchersTesting); + + it('history', utils.detail.historyTesting); + + it('block', utils.detail.blockTesting); + + it('attachments', utils.detail.attachmentTesting); + + describe('custom-fields', utils.detail.customFields.bind(this, 0)); + + describe('related tasks', function() { + it('create', async function() { + let oldRelatedTaskCount = await usDetailHelper.relatedTasks().count(); + + usDetailHelper.createRelatedTasks('test', 1, 1); + + expect(utils.notifications.success.open()).to.be.eventually.true; + + let relatedTaskCount = usDetailHelper.relatedTasks().count(); + + expect(relatedTaskCount).to.be.eventually.equal(oldRelatedTaskCount + 1); + }); + + it('edit', function() { + usDetailHelper.editRelatedTasks(0, 'test2', 2, 2); + + expect(utils.notifications.success.open()).to.be.eventually.true; + }); + + it('delete', async function() { + let oldRelatedTaskCount = await usDetailHelper.relatedTasks().count(); + + usDetailHelper.deleteRelatedTask(0); + + expect(utils.notifications.success.open()).to.be.eventually.true; + + let relatedTaskCount = usDetailHelper.relatedTasks().count(); + + expect(relatedTaskCount).to.be.eventually.equal(oldRelatedTaskCount - 1); + }); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("user-stories", "detail updated"); + }); + + describe('delete & redirect', function() { + it('delete', utils.detail.deleteTesting); + + it('redirected', async function (){ + let url = await browser.getCurrentUrl(); + expect(url).not.to.be.equal(usUrl); + }); + }); +}) diff --git a/e2e/full/wiki.e2e.js b/e2e/full/wiki.e2e.js new file mode 100644 index 00000000..86dbfe6c --- /dev/null +++ b/e2e/full/wiki.e2e.js @@ -0,0 +1,105 @@ +var utils = require('../utils'); +var wikiHelper = require('../helpers').wiki; + +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); + +chai.use(chaiAsPromised); +var expect = chai.expect; + +describe('wiki', function() { + let currentWiki = {}; + + before(async function(){ + browser.get(browser.params.glob.host + 'project/project-0/wiki/home'); + await utils.common.waitLoader(); + }); + + it('screenshot', async function() { + await utils.common.takeScreenshot("wiki", "empty"); + }); + + it('add link', async function(){ + let timestamp = new Date().getTime(); + currentWiki.slug = "test-link" + timestamp; + + let linkText = "Test link" + timestamp; + currentWiki.link = await wikiHelper.links().addLink(linkText); + }); + + it('follow last link', async function() { + // the click event is not on the :( + let lastLink = wikiHelper.links().get().last().$('.link-title'); + browser + .actions() + .mouseMove(lastLink) + .click() + .perform(); + + await utils.common.waitLoader(); + await utils.common.takeScreenshot("wiki", "new-link-created-with-empty-wiki-page"); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host + 'project/project-0/wiki/' + currentWiki.slug); + }); + + it('remove link', async function() { + wikiHelper.links().deleteLink(currentWiki.link); + await utils.common.takeScreenshot("wiki", "deleting-the-created-link"); + }); + + it('edition', async function() { + let timesEdited = wikiHelper.editor().getTimesEdited(); + let lastEditionDatetime = wikiHelper.editor().getLastEditionDateTime(); + wikiHelper.editor().enabledEditionMode(); + let settingText = "This is the new text" + new Date().getTime(); + wikiHelper.editor().setText(settingText); + + //preview + wikiHelper.editor().preview(); + await utils.common.takeScreenshot("wiki", "home-edition-preview"); + + //save + wikiHelper.editor().save(); + let newHtml = await wikiHelper.editor().getInnerHtml(); + let newTimesEdited = wikiHelper.editor().getTimesEdited(); + let newLastEditionDatetime = wikiHelper.editor().getLastEditionDateTime(); + + expect(newHtml).to.be.equal("

" + settingText + "

"); + expect(newTimesEdited).to.be.eventually.equal(timesEdited+1); + expect(newLastEditionDatetime).to.be.not.equal(lastEditionDatetime); + + await utils.common.takeScreenshot("wiki", "home-edition"); + }); + + it('attachments', utils.detail.attachmentTesting); + + it('delete', async function() { + await wikiHelper.editor().delete(); + + expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host + 'project/project-0/wiki/home'); + }); + + it('Custom keyboard actions', async function(){ + wikiHelper.editor().enabledEditionMode(); + + wikiHelper.editor().setText("- aa"); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + let text = await wikiHelper.editor().getText(); + expect(text).to.be.equal("- aa\n- "); + + wikiHelper.editor().setText("- "); + browser.actions().sendKeys(protractor.Key.ENTER).perform(); + text = await wikiHelper.editor().getText(); + expect(text).to.be.equal("\n"); + + wikiHelper.editor().setText("- bbcc"); + browser.actions().sendKeys(protractor.Key.ARROW_LEFT).sendKeys(protractor.Key.ARROW_LEFT).sendKeys(protractor.Key.ENTER).perform(); + text = await wikiHelper.editor().getText(); + expect(text).to.be.equal("- bb\n- cc"); + + wikiHelper.editor().setText("- aa"); + browser.actions().sendKeys(protractor.Key.HOME).sendKeys(protractor.Key.ENTER).perform(); + text = await wikiHelper.editor().getText(); + expect(text).to.be.equal("\n- aa"); + }); +}); diff --git a/e2e/gallery/gallery-front.js b/e2e/gallery/gallery-front.js new file mode 100644 index 00000000..b36fbf9b --- /dev/null +++ b/e2e/gallery/gallery-front.js @@ -0,0 +1,106 @@ +var serverData; + +function alphabetical(a, b) { + var A = a.toLowerCase(); + var B = b.toLowerCase(); + + if (A < B){ + return -1; + }else if (A > B){ + return 1; + }else{ + return 0; + } +} + +$.get('get').then(function(data) { + serverData = data; + + printSections(serverData); +}); + +$('.browsers .browser').click(function() { + $(this).toggleClass('active'); +}); + +$('.browsers .search').click(function() { + var data = serverData; + + // filter by browser + var activeBrowsers = []; + + $('.browsers .active').each(function() { + activeBrowsers.push($(this).data('browser')); + }); + + data = data.filter(function(item) { + return activeBrowsers.indexOf(item.browser) !== -1; + }); + + // filter by section + var section = $('.browsers select').val(); + + if (section !== 'all') { + data = data.filter(function(item) { + return item.section === section; + }); + } + + if(!data.length) { + alert('no images found'); + return; + } + + data.sort(function(a, b) { + return alphabetical(a.title, b.title); + }); + + initGallery(data); +}); + +function printSections(images) { + var sections = []; + + var select = $('.browsers select'); + var options = []; + + var imagesSections = images.reduce(function(sections, image) { + if (sections.indexOf(image.section) === -1) { + sections.push(image.section); + } + + return sections; + }, []); + + imagesSections.forEach(function(section) { + var option = $('