Merge branch 'master' into stable

stable
Alejandro Alonso 2015-11-02 09:44:31 +01:00
commit 0aa85b46e7
559 changed files with 29143 additions and 3880 deletions

2
.gitignore vendored
View File

@ -12,5 +12,5 @@ app/coffee/modules/locales/locale*.coffee
tags tags
tmp/ tmp/
app/config/main.coffee app/config/main.coffee
app/plugins/taiga-front-extras
scss-lint.log scss-lint.log
e2e/screenshots/

View File

@ -1,3 +1,5 @@
sudo: false
before_install: before_install:
- export CHROME_BIN=chromium-browser - export CHROME_BIN=chromium-browser
- export DISPLAY=:99.0 - export DISPLAY=:99.0
@ -11,4 +13,4 @@ before_script:
- gulp deploy - gulp deploy
language: node_js language: node_js
node_js: node_js:
- "0.12" - "4.1"

View File

@ -1,6 +1,6 @@
[main] [main]
host = https://www.transifex.com 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] [taiga-front.locale-enjson]
file_filter = app/locales/locale-<lang>.json file_filter = app/locales/locale-<lang>.json

View File

@ -27,3 +27,5 @@ answer newbie questions, and generally made Taiga that much better:
- Florian Bezagu - Florian Bezagu
- Ryan Swanstrom - Ryan Swanstrom
- Chris Wilson <chris.wilson@aridhia.com> - Chris Wilson <chris.wilson@aridhia.com>
- Brett Profitt <brett.profitt@gmail.com>
- Vlad Topala <topalavlad@gmail.com>

View File

@ -1,6 +1,30 @@
# Changelog # # 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) ## 1.8.0 Saracenia Purpurea (2015-06-18)
### Features ### Features

152
README.md
View File

@ -3,13 +3,107 @@
![Kaleidos Project](http://kaleidos.net/static/img/badge.png "Kaleidos Project") ![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") [![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) [![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 ## ## Get the compiled version ##
You can get the compiled version of this code in the You can get the compiled version of this code in the
[taiga-front-dist](http://github.com/taigaio/taiga-front-dist) repository [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: 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**. Install Sass through your **Terminal or Command Prompt**.
``` ```
$ gem install sass scss-lint gem install sass scss-lint
$ export PATH="~/.gem/ruby/2.1.0/bin:$PATH" export PATH="~/.gem/ruby/2.1.0/bin:$PATH"
$ sass -v // should return Sass 3.3.8 (Maptastic Maple) sass -v # should return Sass 3.3.8 (Maptastic Maple)
``` ```
Complete process for all OS at: http://sass-lang.com/install Complete process for all OS at: http://sass-lang.com/install
**Node + Bower + Gulp** **Node + Bower + Gulp**
We recommend using [nvm](https://github.com/creationix/nvmv) to manage diferent node versions
``` ```
$ sudo npm install -g gulp npm install -g gulp
$ sudo npm install -g bower npm install -g bower
$ npm install npm install
$ bower install bower install
$ gulp gulp
``` ```
And go in your browser to: http://localhost:9001/ 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
```

View File

@ -4,6 +4,8 @@ window.taigaConfig = {
"eventsUrl": null, "eventsUrl": null,
"debug": true, "debug": true,
"defaultLanguage": "en", "defaultLanguage": "en",
"themes": ["taiga", "material-design", "high-contrast"],
"defaultTheme": "taiga",
"publicRegisterEnabled": true, "publicRegisterEnabled": true,
"feedbackEnabled": true, "feedbackEnabled": true,
"privacyPolicyUrl": null, "privacyPolicyUrl": null,

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -66,9 +66,11 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
access: { access: {
requiresLogin: true requiresLogin: true
}, },
loader: true,
title: "HOME.PAGE_TITLE", title: "HOME.PAGE_TITLE",
loader: true,
description: "HOME.PAGE_DESCRIPTION", description: "HOME.PAGE_DESCRIPTION",
loader: true joyride: "dashboard"
} }
) )
@ -100,7 +102,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{ {
templateUrl: "search/search.html", templateUrl: "search/search.html",
reloadOnSearch: false, reloadOnSearch: false,
section: "search" section: "search",
loader: true
} }
) )
@ -108,7 +111,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{ {
templateUrl: "backlog/backlog.html", templateUrl: "backlog/backlog.html",
loader: true, loader: true,
section: "backlog" section: "backlog",
joyride: "backlog"
} }
) )
@ -116,7 +120,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
{ {
templateUrl: "kanban/kanban.html", templateUrl: "kanban/kanban.html",
loader: true, loader: true,
section: "kanban" section: "kanban",
joyride: "kanban"
} }
) )
@ -333,29 +338,25 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
$routeProvider.when("/login", $routeProvider.when("/login",
{ {
templateUrl: "auth/login.html", templateUrl: "auth/login.html",
title: "LOGIN.PAGE_TITLE" title: "LOGIN.PAGE_TITLE",
description: "LOGIN.PAGE_DESCRIPTION" description: "LOGIN.PAGE_DESCRIPTION",
disableHeader: true
} }
) )
$routeProvider.when("/register", $routeProvider.when("/register",
{ {
templateUrl: "auth/register.html", templateUrl: "auth/register.html",
title: "REGISTER.PAGE_TITLE", title: "REGISTER.PAGE_TITLE",
description: "REGISTER.PAGE_DESCRIPTION" description: "REGISTER.PAGE_DESCRIPTION",
disableHeader: true
} }
) )
$routeProvider.when("/forgot-password", $routeProvider.when("/forgot-password",
{ {
templateUrl: "auth/forgot-password.html", templateUrl: "auth/forgot-password.html",
title: "FORGOT_PASSWORD.PAGE_TITLE", title: "FORGOT_PASSWORD.PAGE_TITLE",
description: "FORGOT_PASSWORD.PAGE_DESCRIPTION" description: "FORGOT_PASSWORD.PAGE_DESCRIPTION",
} disableHeader: true
)
$routeProvider.when("/change-password",
{
templateUrl: "auth/change-password-from-recovery.html",
title: "CHANGE_PASSWORD.PAGE_TITLE",
description: "CHANGE_PASSWORD.PAGE_TITLE",
} }
) )
$routeProvider.when("/change-password/:token", $routeProvider.when("/change-password/:token",
@ -363,13 +364,26 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
templateUrl: "auth/change-password-from-recovery.html", templateUrl: "auth/change-password-from-recovery.html",
title: "CHANGE_PASSWORD.PAGE_TITLE", title: "CHANGE_PASSWORD.PAGE_TITLE",
description: "CHANGE_PASSWORD.PAGE_TITLE", description: "CHANGE_PASSWORD.PAGE_TITLE",
disableHeader: true
} }
) )
$routeProvider.when("/invitation/:token", $routeProvider.when("/invitation/:token",
{ {
templateUrl: "auth/invitation.html", templateUrl: "auth/invitation.html",
title: "INVITATION.PAGE_TITLE", 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. # Add next param when user try to access to a secction need auth permissions.
authHttpIntercept = ($q, $location, $navUrls, $lightboxService) -> authHttpIntercept = ($q, $location, $navUrls, $lightboxService) ->
httpResponseError = (response) -> httpResponseError = (response) ->
if response.status == 0 if response.status == 0 || response.status == -1
$lightboxService.closeAll() $lightboxService.closeAll()
$location.path($navUrls.resolve("error")) $location.path($navUrls.resolve("error"))
$location.replace() $location.replace()
else if response.status == 401 else if response.status == 401 and $location.url().indexOf('/login') == -1
nextPath = $location.path() nextUrl = encodeURIComponent($location.url())
$location.url($navUrls.resolve("login")).search("next=#{nextPath}") $location.url($navUrls.resolve("login")).search("next=#{nextUrl}")
return $q.reject(response) return $q.reject(response)
@ -499,9 +513,13 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
.addInterpolation('$translateMessageFormatInterpolation') .addInterpolation('$translateMessageFormatInterpolation')
.preferredLanguage(preferedLangCode) .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 # decoratos
decorators = _.where(@.taigaContribPlugins, {"type": "decorator"}) decorators = _.where(@.taigaContribPlugins, {"type": "decorator"})
@ -544,7 +562,7 @@ i18nInit = (lang, $translate) ->
checksley.updateMessages('default', messages) 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") $log.debug("Initialize application")
# Taiga Plugins # Taiga Plugins
@ -588,7 +606,7 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na
projectService.setSection(next.section) projectService.setSection(next.section)
if next.params.pslug if next.params.pslug
projectService.setProject(next.params.pslug) projectService.setProjectBySlug(next.params.pslug)
else else
projectService.cleanProject() projectService.cleanProject()
@ -597,6 +615,17 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na
description = $translate.instant(next.description or "") description = $translate.instant(next.description or "")
appMetaService.setAll(title, description) 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) pluginsWithModule = _.filter(@.taigaContribPlugins, (plugin) -> plugin.module)
@ -634,6 +663,7 @@ modules = [
"taigaProfile", "taigaProfile",
"taigaHome", "taigaHome",
"taigaUserTimeline", "taigaUserTimeline",
"taigaExternalApps",
# template cache # template cache
"templates", "templates",
@ -641,6 +671,7 @@ modules = [
# Vendor modules # Vendor modules
"ngRoute", "ngRoute",
"ngAnimate", "ngAnimate",
"ngAria",
"pascalprecht.translate", "pascalprecht.translate",
"infinite-scroll", "infinite-scroll",
"tgRepeat" "tgRepeat"
@ -673,5 +704,7 @@ module.run([
"tgAppMetaService", "tgAppMetaService",
"tgProjectService", "tgProjectService",
"tgLoader", "tgLoader",
"tgNavigationBarService",
"$route",
init init
]) ])

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -41,7 +41,7 @@ CreateMembersDirective = ($rs, $rootScope, $confirm, $loading, lightboxService,
template = _.template(""" template = _.template("""
<div class="add-member-wrapper"> <div class="add-member-wrapper">
<fieldset> <fieldset>
<input type="email" placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_TYPE_EMAIL' | translate}}" <input tg-capslock type="email" placeholder="{{'LIGHTBOX.CREATE_MEMBER.PLACEHOLDER_TYPE_EMAIL' | translate}}"
<% if(required) { %> data-required="true" <% } %> data-type="email" /> <% if(required) { %> data-required="true" <% } %> data-type="email" />
</fieldset> </fieldset>
<fieldset> <fieldset>

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 ## Member Avatar Directive
############################################################################# #############################################################################
MembershipsRowAvatarDirective = ($log, $template) -> MembershipsRowAvatarDirective = ($log, $template, $translate) ->
template = $template.get("admin/memberships-row-avatar.html", true) template = $template.get("admin/memberships-row-avatar.html", true)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
pending = $translate.instant("ADMIN.MEMBERSHIP.STATUS_PENDING")
render = (member) -> render = (member) ->
ctx = { ctx = {
full_name: if member.full_name then member.full_name else "" full_name: if member.full_name then member.full_name else ""
email: if member.user_email then member.user_email else member.email email: if member.user_email then member.user_email else member.email
imgurl: if member.photo then member.photo else "/images/unnamed.png" imgurl: if member.photo then member.photo else "/images/unnamed.png"
pending: if !member.is_user_active then pending else ""
} }
html = template(ctx) html = template(ctx)
@ -236,7 +238,7 @@ MembershipsRowAvatarDirective = ($log, $template) ->
return {link: link} 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 = """ pendingTemplate = """
<a class="pending" href=""> <a class="resend" href="">
{{'ADMIN.MEMBERSHIP.STATUS_PENDING' | translate}} {{'ADMIN.MEMBERSHIP.RESEND' | translate}}
<span class="icon icon-reload"></span>
</a> </a>
<a class="delete" href=""> <a class="delete" href="">
<span class="icon icon-delete"></span> <span class="icon icon-delete"></span>
@ -402,9 +403,9 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla
defaultMsg = $translate.instant("ADMIN.MEMBERSHIP.DEFAULT_DELETE_MESSAGE", {email: member.email}) defaultMsg = $translate.instant("ADMIN.MEMBERSHIP.DEFAULT_DELETE_MESSAGE", {email: member.email})
message = if member.user then member.full_name else defaultMsg message = if member.user then member.full_name else defaultMsg
$confirm.askOnDelete(title, message).then (finish) -> $confirm.askOnDelete(title, message).then (askResponse) ->
onSuccess = -> onSuccess = =>
finish() askResponse.finish()
if $scope.page > 1 && ($scope.count - 1) <= $scope.paginatedBy if $scope.page > 1 && ($scope.count - 1) <= $scope.paginatedBy
$ctrl.selectFilter("page", $scope.page - 1) $ctrl.selectFilter("page", $scope.page - 1)
@ -414,8 +415,8 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla
text = $translate.instant("ADMIN.MEMBERSHIP.SUCCESS_DELETE") text = $translate.instant("ADMIN.MEMBERSHIP.SUCCESS_DELETE")
$confirm.notify("success", null, text) $confirm.notify("success", null, text)
onError = -> onError = =>
finish(false) askResponse.finish(false)
text = $translate.instant("ADMIN.MEMBERSHIP.ERROR_DELETE", {message: message}) text = $translate.instant("ADMIN.MEMBERSHIP.ERROR_DELETE", {message: message})
$confirm.notify("error", null, text) $confirm.notify("error", null, text)

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -104,7 +104,7 @@ module.controller("ProjectProfileController", ProjectProfileController)
## Project Profile Directive ## Project Profile Directive
############################################################################# #############################################################################
ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, projectService) -> ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, projectService, currentUserService) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$ctrl = $el.controller() $ctrl = $el.controller()
@ -130,6 +130,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, proje
$ctrl.loadInitialData() $ctrl.loadInitialData()
projectService.fetchProject() projectService.fetchProject()
currentUserService.loadProjects()
promise.then null, (data) -> promise.then null, (data) ->
currentLoading.finish() currentLoading.finish()
@ -144,7 +145,7 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, proje
return {link:link} return {link:link}
module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", "$tgNavUrls", "$tgLocation", 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) -> ProjectModulesDirective = ($repo, $confirm, $loading, projectService) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
form = $el.find("form").checksley()
submit = => submit = =>
form = $el.find("form").checksley()
return if not form.validate() return if not form.validate()
target = angular.element(".admin-functionalities .submit-button") target = angular.element(".admin-functionalities .submit-button")
currentLoading = $loading() currentLoading = $loading()
.target(target) .target(target)
@ -226,7 +228,7 @@ ProjectModulesDirective = ($repo, $confirm, $loading, projectService) ->
else else
$el.find(".videoconference-attributes").addClass("hidden") $el.find(".videoconference-attributes").addClass("hidden")
$scope.project.videoconferences = null $scope.project.videoconferences = null
$scope.project.videoconferences_salt = "" $scope.project.videoconferences_extra_data = ""
$scope.$watch "project", (project) -> $scope.$watch "project", (project) ->
if project.videoconferences? if project.videoconferences?
@ -357,7 +359,7 @@ class CsvExporterController extends taiga.Controller
setCsvUuid: => setCsvUuid: =>
@scope.csvUuid = @scope.project["#{@.type}_csv_uuid"] @scope.csvUuid = @scope.project["#{@.type}_csv_uuid"]
_generateUuid: (finish) => _generateUuid: (response=null) =>
promise = @rs.projects["regenerate_#{@.type}_csv_uuid"](@scope.projectId) promise = @rs.projects["regenerate_#{@.type}_csv_uuid"](@scope.projectId)
promise.then (data) => promise.then (data) =>
@ -367,7 +369,7 @@ class CsvExporterController extends taiga.Controller
@confirm.notify("error") @confirm.notify("error")
promise.finally -> promise.finally ->
finish() response.finish() if response
return promise return promise
regenerateUuid: -> regenerateUuid: ->
@ -377,7 +379,7 @@ class CsvExporterController extends taiga.Controller
@confirm.ask(title, subtitle).then @._generateUuid @confirm.ask(title, subtitle).then @._generateUuid
else else
@._generateUuid(_.identity) @._generateUuid()
class CsvExporterUserstoriesController extends CsvExporterController class CsvExporterUserstoriesController extends CsvExporterController

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 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) class ProjectCustomAttributesController extends mixOf(taiga.Controller, taiga.PageMixin)
@.$inject = [ @.$inject = [
"$scope", "$scope",
@ -390,6 +411,8 @@ class ProjectCustomAttributesController extends mixOf(taiga.Controller, taiga.Pa
constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appMetaService, constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appMetaService,
@translate) -> @translate) ->
@scope.TYPE_CHOICES = TYPE_CHOICES
@scope.project = {} @scope.project = {}
@rootscope.$on "project:loaded", => @rootscope.$on "project:loaded", =>
@ -630,13 +653,11 @@ ProjectCustomAttributesDirective = ($log, $confirm, animationFrame, $translate)
title = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.DELETE") title = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.DELETE")
text = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.CONFIRM_DELETE") text = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.CONFIRM_DELETE")
$confirm.ask(title, text, message).then (finish) -> $confirm.ask(title, text, message).then (response) ->
onSucces = -> onSucces = ->
$ctrl.loadCustomAttributes().finally -> $ctrl.loadCustomAttributes().finally -> response.finish()
finish()
onError = -> onError = ->
finish(false)
$confirm.notify("error", null, "We have not been able to delete '#{message}'.") $confirm.notify("error", null, "We have not been able to delete '#{message}'.")
$ctrl.deleteCustomAttribute(attr).then(onSucces, onError) $ctrl.deleteCustomAttribute(attr).then(onSucces, onError)

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 return @repo.remove(@scope.role, {moveTo: response.selected}).then onSuccess, onError
setComputable: debounce 2000, -> _enableComputable: =>
onSuccess = => onSuccess = =>
@confirm.notify("success") @confirm.notify("success")
@.loadProject() @.loadProject()
@ -140,9 +140,37 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil
@repo.save(@scope.role).then onSuccess, onError @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) module.controller("RolesController", RolesController)
EditRoleDirective = ($repo, $confirm) -> EditRoleDirective = ($repo, $confirm) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
toggleView = -> toggleView = ->

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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") title = $translate.instant("ADMIN.WEBHOOKS.DELETE")
message = $translate.instant("ADMIN.WEBHOOKS.WEBHOOK_NAME", {name: webhook.name}) message = $translate.instant("ADMIN.WEBHOOKS.WEBHOOK_NAME", {name: webhook.name})
$confirm.askOnDelete(title, message).then (finish) => $confirm.askOnDelete(title, message).then (askResponse) =>
onSucces = -> onSucces = ->
finish() askResponse.finish()
$scope.$emit("webhooks:reload") $scope.$emit("webhooks:reload")
onError = -> onError = ->
finish(false) askResponse.finish(false)
$confirm.notify("error") $confirm.notify("error")
$repo.remove(webhook).then(onSucces, onError) $repo.remove(webhook).then(onSucces, onError)

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -37,10 +37,15 @@ class AuthService extends taiga.Service
"$tgUrls", "$tgUrls",
"$tgConfig", "$tgConfig",
"$translate", "$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() super()
@._currentTheme = @config.get("defaultTheme") || "taiga" # load on index.jade
userModel = @.getUser() userModel = @.getUser()
@.setUserdata(userModel) @.setUserdata(userModel)
@ -51,9 +56,18 @@ class AuthService extends taiga.Service
else else
@.userData = null @.userData = null
_getUserTheme: ->
return @rootscope.user?.theme || @config.get("defaultTheme") || "taiga"
_setTheme: ->
newTheme = @._getUserTheme()
if @._currentTheme != newTheme
@._currentTheme = newTheme
@themeService.use(@._currentTheme)
_setLocales: -> _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.preferredLanguage(lang) # Needed for calls to the api in the correct language
@translate.use(lang) # Needed for change the interface in runtime @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) user = @model.make_model("users", userData)
@rootscope.user = user @rootscope.user = user
@._setLocales() @._setLocales()
@._setTheme()
return user return user
return null return null
@ -78,6 +95,7 @@ class AuthService extends taiga.Service
@.setUserdata(user) @.setUserdata(user)
@._setLocales() @._setLocales()
@._setTheme()
clear: -> clear: ->
@rootscope.auth = null @rootscope.auth = null
@ -117,9 +135,12 @@ class AuthService extends taiga.Service
logout: -> logout: ->
@.removeToken() @.removeToken()
@.clear() @.clear()
@currentUserService.removeUser() @currentUserService.removeUser()
@._setTheme()
@._setLocales()
register: (data, type, existing) -> register: (data, type, existing) ->
url = @urls.resolve("auth-register") url = @urls.resolve("auth-register")
@ -200,12 +221,12 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
onSuccess = (response) -> onSuccess = (response) ->
if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login") if $routeParams['next'] and $routeParams['next'] != $navUrls.resolve("login")
nextUrl = $routeParams['next'] nextUrl = decodeURIComponent($routeParams['next'])
else else
nextUrl = $navUrls.resolve("home") nextUrl = $navUrls.resolve("home")
$events.setupConnection() $events.setupConnection()
$location.path(nextUrl) $location.url(nextUrl)
onError = (response) -> onError = (response) ->
$confirm.notify("light-error", $translate.instant("LOGIN_FORM.ERROR_AUTH_INCORRECT")) $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.tokenInParams = true
$scope.data.token = $params.token $scope.data.token = $params.token
else 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() form = $el.find("form").checksley()
@ -354,7 +378,7 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav
$confirm.success(text) $confirm.success(text)
onErrorSubmit = (response) -> 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) $confirm.notify("light-error", text)
submit = debounce 2000, (event) => submit = debounce 2000, (event) =>
@ -394,7 +418,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics
$location.path($navUrls.resolve("login")) $location.path($navUrls.resolve("login"))
text = $translate.instant("INVITATION_LOGIN_FORM.NOT_FOUND") text = $translate.instant("INVITATION_LOGIN_FORM.NOT_FOUND")
$confirm.success(text) $confirm.notify("light-error", text)
# Login form # Login form
$scope.dataLogin = {token: token} $scope.dataLogin = {token: token}

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -35,11 +35,13 @@ module = angular.module("taigaBacklog")
## Issues Filters Directive ## Issues Filters Directive
############################################################################# #############################################################################
BacklogFiltersDirective = ($log, $location, $templates) -> BacklogFiltersDirective = ($q, $log, $location, $templates) ->
template = $templates.get("backlog/filters.html", true) template = $templates.get("backlog/filters.html", true)
templateSelected = $templates.get("backlog/filter-selected.html", true) templateSelected = $templates.get("backlog/filter-selected.html", true)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
currentFiltersType = ''
$ctrl = $el.closest(".wrapper").controller() $ctrl = $el.closest(".wrapper").controller()
selectedFilters = [] 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").html(title)
$el.find("h2 a.subfilter span.title").prop("data-type", type) $el.find("h2 a.subfilter span.title").prop("data-type", type)
currentFiltersType = getFiltersType()
showCategories = -> showCategories = ->
$el.find(".filters-cats").show() $el.find(".filters-cats").show()
$el.find(".filter-list").addClass("hidden") $el.find(".filter-list").addClass("hidden")
$el.find("h2.breadcrumb").addClass("hidden") $el.find("h2.breadcrumb").addClass("hidden")
initializeSelectedFilters = (filters) -> initializeSelectedFilters = () ->
showCategories() showCategories()
selectedFilters = [] selectedFilters = []
for name, values of filters for name, values of $scope.filters
for val in values for val in values
selectedFilters.push(val) if val.selected selectedFilters.push(val) if val.selected
@ -81,43 +85,62 @@ BacklogFiltersDirective = ($log, $location, $templates) ->
html = template({filters:filters}) html = template({filters:filters})
$el.find(".filter-list").html(html) $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) -> toggleFilterSelection = (type, id) ->
currentFiltersType = getFiltersType()
filters = $scope.filters[type] filters = $scope.filters[type]
filter = _.find(filters, {id: taiga.toString(id)}) filter = _.find(filters, {id: id})
filter.selected = (not filter.selected) filter.selected = (not filter.selected)
if filter.selected if filter.selected
selectedFilters.push(filter) selectedFilters.push(filter)
$scope.$apply -> $scope.$apply ->
$ctrl.selectFilter(type, id) $ctrl.selectFilter(type, id)
else else
selectedFilters = _.reject(selectedFilters, filter) selectedFilters = _.reject selectedFilters, (selected) ->
$scope.$apply -> return filter.type == selected.type && filter.id == selected.id
$ctrl.unselectFilter(type, id) $ctrl.unselectFilter(type, id)
renderSelectedFilters(selectedFilters) renderSelectedFilters(selectedFilters)
currentFiltersType = $el.find("h2 a.subfilter span.title").prop('data-type')
if type == currentFiltersType if type == currentFiltersType
renderFilters(_.reject(filters, "selected")) renderFilters(_.reject(filters, "selected"))
$ctrl.loadUserstories() reloadUserstories()
selectQFilter = debounceLeading 100, (value) -> selectQFilter = debounceLeading 100, (value) ->
return if value is undefined return if value is undefined
if value.length == 0 if value.length == 0
$ctrl.replaceFilter("q", null) $ctrl.replaceFilter("q", null)
else else
$ctrl.replaceFilter("q", value) $ctrl.replaceFilter("q", value)
$ctrl.loadUserstories()
reloadUserstories()
$scope.$watch("filtersQ", selectQFilter) $scope.$watch("filtersQ", selectQFilter)
## Angular Watchers ## Angular Watchers
$scope.$on "filters:loaded", (ctx, filters) -> $scope.$on "backlog:loaded", (ctx) ->
initializeSelectedFilters(filters) initializeSelectedFilters()
$scope.$on "filters:update", (ctx, filters) -> $scope.$on "filters:update", (ctx) ->
renderFilters(filters) $ctrl.generateFilters().then () ->
filters = $scope.filters[currentFiltersType]
if currentFiltersType
renderFilters(_.reject(filters, "selected"))
## Dom Event Handlers ## Dom Event Handlers
$el.on "click", ".filters-cats > ul > li > a", (event) -> $el.on "click", ".filters-cats > ul > li > a", (event) ->
@ -126,7 +149,7 @@ BacklogFiltersDirective = ($log, $location, $templates) ->
tags = $scope.filters[target.data("type")] tags = $scope.filters[target.data("type")]
renderFilters(_.reject(tags, "selected")) 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) -> $el.on "click", ".filters-inner > .filters-step-cat > .breadcrumb > .back", (event) ->
event.preventDefault() event.preventDefault()
@ -153,4 +176,4 @@ BacklogFiltersDirective = ($log, $location, $templates) ->
return {link:link} return {link:link}
module.directive("tgBacklogFilters", ["$log", "$tgLocation", "$tgTemplate", BacklogFiltersDirective]) module.directive("tgBacklogFilters", ["$q", "$log", "$tgLocation", "$tgTemplate", BacklogFiltersDirective])

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -34,6 +34,7 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading,
hasErrors = false hasErrors = false
createSprint = true createSprint = true
resetSprint = () ->
$scope.sprint = { $scope.sprint = {
project: null project: null
name: null name: null
@ -95,19 +96,30 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading,
title = $translate.instant("LIGHTBOX.DELETE_SPRINT.TITLE") title = $translate.instant("LIGHTBOX.DELETE_SPRINT.TITLE")
message = $scope.sprint.name message = $scope.sprint.name
$confirm.askOnDelete(title, message).then (finish) => $confirm.askOnDelete(title, message).then (askResponse) =>
onSuccess = -> onSuccess = ->
finish() askResponse.finish()
$scope.milestonesCounter -= 1 $scope.milestonesCounter -= 1
lightboxService.close($el) lightboxService.close($el)
$rootscope.$broadcast("sprintform:remove:success") $rootscope.$broadcast("sprintform:remove:success", $scope.sprint)
onError = -> onError = ->
finish(false) askResponse.finish(false)
$confirm.notify("error") $confirm.notify("error")
$repo.remove($scope.sprint).then(onSuccess, onError) $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) -> $scope.$on "sprintform:create", (event, projectId) ->
resetSprint()
form = $el.find("form").checksley() form = $el.find("form").checksley()
form.reset() form.reset()
@ -117,20 +129,24 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading,
$scope.sprint.name = null $scope.sprint.name = null
$scope.sprint.slug = null $scope.sprint.slug = null
lastSprint = $scope.sprints[0] lastSprint = getLastSprint()
estimatedStart = moment() estimatedStart = moment()
if $scope.sprint.estimated_start
estimatedStart = moment($scope.sprint.estimated_start) if lastSprint
else if lastSprint?
estimatedStart = moment(lastSprint.estimated_finish) estimatedStart = moment(lastSprint.estimated_finish)
else if $scope.sprint.estimated_start
estimatedStart = moment($scope.sprint.estimated_start)
$scope.sprint.estimated_start = estimatedStart.format(prettyDate) $scope.sprint.estimated_start = estimatedStart.format(prettyDate)
estimatedFinish = moment().add(2, "weeks") estimatedFinish = moment().add(2, "weeks")
if $scope.sprint.estimated_finish
estimatedFinish = moment($scope.sprint.estimated_finish) if lastSprint
else if lastSprint?
estimatedFinish = moment(lastSprint.estimated_finish).add(2, "weeks") 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) $scope.sprint.estimated_finish = estimatedFinish.format(prettyDate)
lastSprintNameDom = $el.find(".last-sprint-name") lastSprintNameDom = $el.find(".last-sprint-name")
@ -152,6 +168,8 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading,
$el.find(".last-sprint-name").removeClass("disappear") $el.find(".last-sprint-name").removeClass("disappear")
$scope.$on "sprintform:edit", (ctx, sprint) -> $scope.$on "sprintform:edit", (ctx, sprint) ->
resetSprint()
createSprint = false createSprint = false
prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT") prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT")
@ -187,6 +205,8 @@ CreateEditSprint = ($repo, $confirm, $rs, $rootscope, lightboxService, $loading,
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
resetSprint()
return {link: link} return {link: link}

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -28,6 +28,7 @@ bindOnce = @.taiga.bindOnce
groupBy = @.taiga.groupBy groupBy = @.taiga.groupBy
timeout = @.taiga.timeout timeout = @.taiga.timeout
bindMethods = @.taiga.bindMethods bindMethods = @.taiga.bindMethods
generateHash = @.taiga.generateHash
module = angular.module("taigaBacklog") 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") @scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME")
@showTags = false @showTags = false
@activeFilters = false @activeFilters = false
@scope.showGraphPlaceholder = null
@.initializeEventHandlers() @.initializeEventHandlers()
@ -96,18 +98,26 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.$on "usform:new:success", => @scope.$on "usform:new:success", =>
@.loadUserstories() @.loadUserstories()
@.loadProjectStats() @.loadProjectStats()
@rootscope.$broadcast("filters:update")
@analytics.trackEvent("userstory", "create", "create userstory on backlog", 1) @analytics.trackEvent("userstory", "create", "create userstory on backlog", 1)
@scope.$on "sprintform:edit:success", => @scope.$on "sprintform:edit:success", =>
@.loadProjectStats() @.loadProjectStats()
@scope.$on "sprintform:remove:success", => @scope.$on "sprintform:remove:success", (event, sprint) =>
@.loadSprints() @.loadSprints()
@.loadProjectStats() @.loadProjectStats()
@.loadUserstories() @.loadUserstories()
if sprint.closed
@.loadClosedSprints()
@rootscope.$broadcast("filters:update")
@scope.$on "usform:edit:success", => @scope.$on "usform:edit:success", =>
@.loadUserstories() @.loadUserstories()
@rootscope.$broadcast("filters:update")
@scope.$on("sprint:us:move", @.moveUs) @scope.$on("sprint:us:move", @.moveUs)
@scope.$on("sprint:us:moved", @.loadSprints) @scope.$on("sprint:us:moved", @.loadSprints)
@ -137,18 +147,16 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
loadProjectStats: -> loadProjectStats: ->
return @rs.projects.stats(@scope.projectId).then (stats) => return @rs.projects.stats(@scope.projectId).then (stats) =>
@scope.stats = stats @scope.stats = stats
totalPoints = if stats.total_points then stats.total_points else stats.defined_points
if stats.total_points if totalPoints
@scope.stats.completedPercentage = Math.round(100 * stats.closed_points / stats.total_points) @scope.stats.completedPercentage = Math.round(100 * stats.closed_points / totalPoints)
else else
@scope.stats.completedPercentage = 0 @scope.stats.completedPercentage = 0
@scope.showGraphPlaceholder = !(stats.total_points? && stats.total_milestones?)
return stats return stats
refreshTagsColors: ->
return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) =>
@scope.project.tags_colors = tags_colors
unloadClosedSprints: -> unloadClosedSprints: ->
@scope.$apply => @scope.$apply =>
@scope.closedSprints = [] @scope.closedSprints = []
@ -156,17 +164,29 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
loadClosedSprints: -> loadClosedSprints: ->
params = {closed: true} 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 # NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files
for sprint in sprints for sprint in sprints
sprint.user_stories = _.sortBy(sprint.user_stories, "sprint_order") sprint.user_stories = _.sortBy(sprint.user_stories, "sprint_order")
@scope.closedSprints = sprints @scope.closedSprints = sprints
@scope.closedSprintsById = groupBy(sprints, (x) -> x.id)
@rootscope.$broadcast("closed-sprints:reloaded", sprints) @rootscope.$broadcast("closed-sprints:reloaded", sprints)
return sprints return sprints
loadSprints: -> loadSprints: ->
params = {closed: false} 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 # NOTE: Fix order of USs because the filter orderBy does not work propertly in partials files
for sprint in sprints for sprint in sprints
sprint.user_stories = _.sortBy(sprint.user_stories, "sprint_order") sprint.user_stories = _.sortBy(sprint.user_stories, "sprint_order")
@ -182,7 +202,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
resetFilters: -> resetFilters: ->
selectedTags = _.filter(@scope.filters.tags, "selected") selectedTags = _.filter(@scope.filters.tags, "selected")
selectedStatuses = _.filter(@scope.filters.statuses, "selected") selectedStatuses = _.filter(@scope.filters.status, "selected")
@scope.filtersQ = "" @scope.filtersQ = ""
@ -195,23 +215,20 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@.unselectFilter(item.type, item.id) @.unselectFilter(item.type, item.id)
@.loadUserstories() @.loadUserstories()
@rootscope.$broadcast("filters:update")
loadUserstories: -> loadUserstories: ->
@scope.httpParams = @.getUrlFilters() @scope.httpParams = @.getUrlFilters()
@rs.userstories.storeQueryParams(@scope.projectId, @scope.httpParams) @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) => return promise.then (userstories) =>
userstories = data[1]
# NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files # NOTE: Fix order of USs because the filter orderBy does not work propertly in the partials files
@scope.userstories = _.sortBy(userstories, "backlog_order") @scope.userstories = _.sortBy(userstories, "backlog_order")
@.setSearchDataFilters() @.setSearchDataFilters()
@.filterVisibleUserstories()
@.generateFilters()
@rootscope.$broadcast("filters:loaded", @scope.filters)
# The broadcast must be executed when the DOM has been fully reloaded. # 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 # We can't assure when this exactly happens so we need a defer
scopeDefer @scope, => scopeDefer @scope, =>
@ -233,7 +250,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.projectId = project.id @scope.projectId = project.id
@scope.project = project @scope.project = project
@scope.totalClosedMilestones = project.total_closed_milestones @scope.closedMilestones = !!project.total_closed_milestones
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
@scope.points = _.sortBy(project.points, "order") @scope.points = _.sortBy(project.points, "order")
@scope.pointsById = groupBy(project.points, (x) -> x.id) @scope.pointsById = groupBy(project.points, (x) -> x.id)
@ -244,25 +261,13 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
loadInitialData: -> loadInitialData: ->
promise = @.loadProject() promise = @.loadProject()
promise.then (project) => promise.then (project) =>
@.fillUsersAndRoles(project.users, project.roles) @.fillUsersAndRoles(project.members, project.roles)
@.initializeSubscription() @.initializeSubscription()
return promise.then(=> @.loadBacklog()) return promise
.then(=> @.loadBacklog())
filterVisibleUserstories: -> .then(=> @.generateFilters())
@scope.visibleUserstories = [] .then(=> @scope.$emit("backlog:loaded"))
# 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
prepareBulkUpdateData: (uses, field="backlog_order") -> prepareBulkUpdateData: (uses, field="backlog_order") ->
return _.map(uses, (x) -> {"us_id": x.id, "order": x[field]}) 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 oldSprintId = usList[0].milestone
project = usList[0].project 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 # In the same sprint or in the backlog
if newSprintId == oldSprintId if newSprintId == oldSprintId
items = null items = null
@ -289,7 +311,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
if newSprintId == null if newSprintId == null
userstories = @scope.userstories userstories = @scope.userstories
else else
userstories = @scope.sprintsById[newSprintId].user_stories userstories = newSprint.user_stories
@scope.$apply -> @scope.$apply ->
for us, key in usList for us, key in usList
@ -333,16 +355,9 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.$apply => @scope.$apply =>
# Add new us to backlog userstories list # Add new us to backlog userstories list
# @scope.userstories.splice(newUsIndex, 0, us) # @scope.userstories.splice(newUsIndex, 0, us)
# @scope.visibleUserstories.splice(newUsIndex, 0, us)
args = [newUsIndex, 0].concat(usList) args = [newUsIndex, 0].concat(usList)
Array.prototype.splice.apply(@scope.userstories, args) 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 for us, key in usList
r = sprint.user_stories.indexOf(us) r = sprint.user_stories.indexOf(us)
sprint.user_stories.splice(r, 1) 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 => return @rs.userstories.bulkUpdateBacklogOrder(us.project, data).then =>
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
if movedFromClosedSprint
@rootscope.$broadcast("backlog:load-closed-sprints")
promise.then null, -> promise.then null, ->
console.log "FAIL" # TODO console.log "FAIL" # TODO
return promise return promise
# From backlog to sprint # From backlog to sprint
newSprint = @scope.sprintsById[newSprintId]
if oldSprintId == null if oldSprintId == null
us.milestone = newSprintId for us in usList 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. # Remove moving us from backlog userstories lists.
for us, key in usList for us, key in usList
r = @scope.visibleUserstories.indexOf(us)
@scope.visibleUserstories.splice(r, 1)
r = @scope.userstories.indexOf(us) r = @scope.userstories.indexOf(us)
@scope.userstories.splice(r, 1) @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. # Remove the us from the sprint list.
for us in usList for us in usList
oldSprint = @scope.sprintsById[oldSprintId] r = sprint.user_stories.indexOf(us)
r = oldSprint.user_stories.indexOf(us) sprint.user_stories.splice(r, 1)
oldSprint.user_stories.splice(r, 1)
# Persist the milestone change of userstory # Persist the milestone change of userstory
promises = _.map usList, (us) => @repo.save(us) 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") items = @.resortUserStories(newSprint.user_stories, "sprint_order")
data = @.prepareBulkUpdateData(items, "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) @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
@rs.userstories.bulkUpdateBacklogOrder(project, data).then => @rs.userstories.bulkUpdateBacklogOrder(project, data).then =>
for us in usList for us in usList
@rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId) @rootscope.$broadcast("sprint:us:moved", us, oldSprintId, newSprintId)
if movedToClosedSprint || movedFromClosedSprint
@scope.$broadcast("backlog:load-closed-sprints")
promise.then null, -> promise.then null, ->
console.log "FAIL" # TODO console.log "FAIL" # TODO
@ -439,58 +455,43 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@searchdata[name][val] = true @searchdata[name][val] = true
getUrlFilters: -> getUrlFilters: ->
return _.pick(@location.search(), "statuses", "tags", "q") return _.pick(@location.search(), "status", "tags", "q")
generateFilters: -> generateFilters: ->
urlfilters = @.getUrlFilters() urlfilters = @.getUrlFilters()
@scope.filters = {} @scope.filters = {}
#tags loadFilters = {}
plainTags = _.flatten(_.filter(_.map(@scope.visibleUserstories, "tags"))) loadFilters.project = @scope.projectId
plainTags.sort() loadFilters.tags = urlfilters.tags
loadFilters.status = urlfilters.status
loadFilters.q = urlfilters.q
loadFilters.milestone = 'null'
if plainTags.length == 0 and urlfilters["tags"] return @rs.userstories.filtersData(loadFilters).then (data) =>
plainTags.push(urlfilters["tags"]) choicesFiltersFormat = (choices, type, byIdObject) =>
_.map choices, (t) ->
t.type = type
return t
@scope.filters.tags = _.map _.countBy(plainTags), (v, k) => tagsFilterFormat = (tags) =>
obj = { return _.map tags, (t) ->
id: k, t.id = t.name
type: "tags", t.type = 'tags'
name: k, return t
color: @scope.project.tags_colors[k],
count: v # Build filters data structure
} @scope.filters.status = choicesFiltersFormat(data.statuses, "status", @scope.usStatusById)
obj.selected = true if @isFilterSelected("tags", obj.id) @scope.filters.tags = tagsFilterFormat(data.tags)
return obj
selectedTags = _.filter(@scope.filters.tags, "selected") selectedTags = _.filter(@scope.filters.tags, "selected")
selectedTags = _.map(selectedTags, "name") selectedTags = _.map(selectedTags, "id")
#status selectedStatuses = _.filter(@scope.filters.status, "selected")
plainStatuses = _.map(@scope.visibleUserstories, "status")
plainStatuses = _.filter plainStatuses, (status) =>
if status
return status
if plainStatuses.length == 0 and urlfilters["statuses"]
plainStatuses.push(urlfilters["statuses"])
@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)
return obj
selectedStatuses = _.filter(@scope.filters.statuses, "selected")
selectedStatuses = _.map(selectedStatuses, "id") selectedStatuses = _.map(selectedStatuses, "id")
@.markSelectedFilters(@scope.filters, urlfilters)
#store query params #store query params
@rs.userstories.storeQueryParams(@scope.projectId, { @rs.userstories.storeQueryParams(@scope.projectId, {
"status": selectedStatuses, "status": selectedStatuses,
@ -499,13 +500,31 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
"milestone": null "milestone": null
}) })
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] = {}
for val in "#{value}".split(",")
searchdata[name][val] = true
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 ## Template actions
updateUserStoryStatus: () -> updateUserStoryStatus: () ->
@.setSearchDataFilters() @.setSearchDataFilters()
@.filterVisibleUserstories() @.generateFilters().then () =>
@.generateFilters() @rootscope.$broadcast("filters:update")
@rootscope.$broadcast("filters:update", @scope.filters['statuses'])
@.loadProjectStats() @.loadProjectStats()
editUserStory: (projectId, ref, $event) -> editUserStory: (projectId, ref, $event) ->
@ -527,16 +546,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
message = us.subject 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 # We modify the userstories in scope so the user doesn't see the removed US for a while
@scope.userstories = _.without(@scope.userstories, us) @scope.userstories = _.without(@scope.userstories, us)
@filterVisibleUserstories()
promise = @.repo.remove(us) promise = @.repo.remove(us)
promise.then => promise.then =>
finish() askResponse.finish()
@.loadBacklog() @.loadBacklog()
promise.then null, => promise.then null, =>
finish(false) askResponse.finish(false)
@confirm.notify("error") @confirm.notify("error")
addNewUs: (type) -> addNewUs: (type) ->
@ -563,7 +581,7 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
linkDoomLine = ($scope, $el, $attrs, $ctrl) -> linkDoomLine = ($scope, $el, $attrs, $ctrl) ->
reloadDoomLine = -> reloadDoomLine = ->
if $scope.stats? if $scope.stats? and $scope.stats.total_points? and $scope.stats.total_points != 0
removeDoomlineDom() removeDoomlineDom()
stats = $scope.stats stats = $scope.stats
@ -571,9 +589,9 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
total_points = stats.total_points total_points = stats.total_points
current_sum = stats.assigned_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 current_sum += us.total_points
if current_sum > total_points if current_sum > total_points
@ -614,7 +632,6 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
# Update the total of points # Update the total of points
$scope.sprints[0].total_points += totalExtraPoints $scope.sprints[0].total_points += totalExtraPoints
$ctrl.filterVisibleUserstories()
$repo.saveAll(selectedUss).then -> $repo.saveAll(selectedUss).then ->
$ctrl.loadSprints() $ctrl.loadSprints()
$ctrl.loadProjectStats() $ctrl.loadProjectStats()
@ -626,7 +643,7 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
checkSelected = (target) -> checkSelected = (target) ->
lastChecked = target.closest(".us-item-row") lastChecked = target.closest(".us-item-row")
moveToCurrentSprintDom = $el.find("#move-to-current-sprint") 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 if selectedUsDom.length > 0 and $scope.sprints.length > 0
moveToCurrentSprintDom.show() moveToCurrentSprintDom.show()
@ -641,7 +658,7 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
return true return true
# Enable move to current sprint only when there are selected us's # 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 # check elements between the last two if shift is pressed
if lastChecked && shiftPressed if lastChecked && shiftPressed
elements = [] elements = []
@ -656,15 +673,16 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
_.map elements, (elm) -> _.map elements, (elm) ->
input = $(elm).find("input:checkbox") input = $(elm).find("input:checkbox")
input.prop('checked', true); input.prop('checked', true)
checkSelected(input) checkSelected(input)
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
target.closest(".us-item-row").toggleClass('is-checked')
checkSelected(target) checkSelected(target)
$el.on "click", "#move-to-current-sprint", (event) => $el.on "click", "#move-to-current-sprint", (event) =>
# Calculating the us's to be modified # 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) -> ussToMove = _.map ussDom, (item) ->
item = $(item).closest('.tg-scope') item = $(item).closest('.tg-scope')
@ -688,12 +706,12 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
elm.addClass("active") elm.addClass("active")
text = $translate.instant("BACKLOG.TAGS.HIDE") text = $translate.instant("BACKLOG.TAGS.HIDE")
elm.find(".text").text(text) elm.text(text)
else else
elm.removeClass("active") elm.removeClass("active")
text = $translate.instant("BACKLOG.TAGS.SHOW") text = $translate.instant("BACKLOG.TAGS.SHOW")
elm.find(".text").text(text) elm.text(text)
showHideFilter = ($scope, $el, $ctrl) -> showHideFilter = ($scope, $el, $ctrl) ->
sidebar = $el.find("sidebar.filters-bar") sidebar = $el.find("sidebar.filters-bar")
@ -736,8 +754,7 @@ BacklogDirective = ($repo, $rootscope, $translate) ->
$el.find(".backlog-table-body").disableSelection() $el.find(".backlog-table-body").disableSelection()
filters = $ctrl.getUrlFilters() filters = $ctrl.getUrlFilters()
if filters.status ||
if filters.statuses ||
filters.tags || filters.tags ||
filters.q filters.q
showHideFilter($scope, $el, $ctrl) showHideFilter($scope, $el, $ctrl)
@ -915,6 +932,56 @@ UsPointsDirective = ($tgEstimationsService, $repo, $tgTemplate) ->
module.directive("tgBacklogUsPoints", ["$tgEstimationsService", "$tgRepo", "$tgTemplate", UsPointsDirective]) 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 ## Burndown graph directive
############################################################################# #############################################################################
@ -1007,16 +1074,16 @@ BurndownBacklogGraphDirective = ($translate) ->
tooltipOpts: { tooltipOpts: {
content: (label, xval, yval, flotItem) -> content: (label, xval, yval, flotItem) ->
if flotItem.seriesIndex == 1 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) return $translate.instant("BACKLOG.CHART.OPTIMAL", ctx)
else if flotItem.seriesIndex == 2 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) return $translate.instant("BACKLOG.CHART.REAL", ctx)
else if flotItem.seriesIndex == 3 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) return $translate.instant("BACKLOG.CHART.INCREMENT_TEAM", ctx)
else 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) return $translate.instant("BACKLOG.CHART.INCREMENT_CLIENT", ctx)
} }
} }
@ -1046,14 +1113,16 @@ module.directive("tgBurndownBacklogGraph", ["$translate", BurndownBacklogGraphDi
## Backlog progress bar directive ## Backlog progress bar directive
############################################################################# #############################################################################
TgBacklogProgressBarDirective = ($template) -> TgBacklogProgressBarDirective = ($template, $compile) ->
template = $template.get("backlog/progress-bar.html", true) template = $template.get("backlog/progress-bar.html", true)
render = (el, projectPointsPercentaje, closedPointsPercentaje) -> render = (scope, el, projectPointsPercentaje, closedPointsPercentaje) ->
el.html(template({ html = template({
projectPointsPercentaje: projectPointsPercentaje, projectPointsPercentaje: projectPointsPercentaje,
closedPointsPercentaje:closedPointsPercentaje closedPointsPercentaje:closedPointsPercentaje
})) })
html = $compile(html)(scope)
el.html(html)
adjustPercentaje = (percentage) -> adjustPercentaje = (percentage) ->
adjusted = _.max([0 , percentage]) adjusted = _.max([0 , percentage])
@ -1065,7 +1134,7 @@ TgBacklogProgressBarDirective = ($template) ->
$scope.$watch $attrs.tgBacklogProgressBar, (stats) -> $scope.$watch $attrs.tgBacklogProgressBar, (stats) ->
if stats? if stats?
totalPoints = stats.total_points totalPoints = if stats.total_points then stats.total_points else stats.defined_points
definedPoints = stats.defined_points definedPoints = stats.defined_points
closedPoints = stats.closed_points closedPoints = stats.closed_points
if definedPoints > totalPoints if definedPoints > totalPoints
@ -1077,11 +1146,11 @@ TgBacklogProgressBarDirective = ($template) ->
projectPointsPercentaje = adjustPercentaje(projectPointsPercentaje - 3) projectPointsPercentaje = adjustPercentaje(projectPointsPercentaje - 3)
closedPointsPercentaje = adjustPercentaje(closedPointsPercentaje - 3) closedPointsPercentaje = adjustPercentaje(closedPointsPercentaje - 3)
render($el, projectPointsPercentaje, closedPointsPercentaje) render($scope, $el, projectPointsPercentaje, closedPointsPercentaje)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
return {link: link} return {link: link}
module.directive("tgBacklogProgressBar", ["$tgTemplate", TgBacklogProgressBarDirective]) module.directive("tgBacklogProgressBar", ["$tgTemplate", "$compile", TgBacklogProgressBarDirective])

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 the user has not enough permissions we don't enable the sortable
if project.my_permissions.indexOf("modify_us") > -1 if project.my_permissions.indexOf("modify_us") > -1
$el.sortable({ $el.sortable({
items: ".us-item-row",
dropOnEmpty: true dropOnEmpty: true
}) })

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -35,12 +35,6 @@ BacklogSprintDirective = ($repo, $rootscope) ->
easing: 'linear' easing: 'linear'
} }
refreshSprintTableHeight = (sprintTable) =>
if !sprintTable.find(".row").length
sprintTable.css("height", sprintTableMinHeight)
else
sprintTable.css("height", "auto")
toggleSprint = ($el) => toggleSprint = ($el) =>
sprintTable = $el.find(".sprint-table") sprintTable = $el.find(".sprint-table")
sprintArrow = $el.find(".icon-arrow-up") sprintArrow = $el.find(".icon-arrow-up")
@ -48,8 +42,6 @@ BacklogSprintDirective = ($repo, $rootscope) ->
sprintArrow.toggleClass('active') sprintArrow.toggleClass('active')
sprintTable.toggleClass('open') sprintTable.toggleClass('open')
refreshSprintTableHeight(sprintTable)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$scope.$watch $attrs.tgBacklogSprint, (sprint) -> $scope.$watch $attrs.tgBacklogSprint, (sprint) ->
sprint = $scope.$eval($attrs.tgBacklogSprint) sprint = $scope.$eval($attrs.tgBacklogSprint)

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -71,16 +71,39 @@ NavigationUrlsDirective = ($navurls, $auth, $q, $location) ->
parseNav = (data, $scope) -> parseNav = (data, $scope) ->
[name, params] = _.map(data.split(":"), trim) [name, params] = _.map(data.split(":"), trim)
if params 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 else
params = [] params = []
values = _.map(params, (x) -> trim(x.split("=")[1]))
values = _.map params, (param) -> _.values(param)[0]
promises = _.map(values, (x) -> bindOnceP($scope, x)) promises = _.map(values, (x) -> bindOnceP($scope, x))
return $q.all(promises).then -> return $q.all(promises).then ->
options = {} options = {}
for item in params for param in params
[key, value] = _.map(item.split("="), trim) key = Object.keys(param)[0]
value = param[key]
options[key] = $scope.$eval(value) options[key] = $scope.$eval(value)
return [name, options] return [name, options]

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -140,7 +140,7 @@ class RepositoryService extends taiga.Service
return defered.promise return defered.promise
queryMany: (name, params, options={}) -> queryMany: (name, params, options={}, headers=false) ->
url = @urls.resolve(name) url = @urls.resolve(name)
httpOptions = {headers: {}} httpOptions = {headers: {}}
@ -148,7 +148,12 @@ class RepositoryService extends taiga.Service
httpOptions.headers["x-disable-pagination"] = "1" httpOptions.headers["x-disable-pagination"] = "1"
return @http.get(url, params, httpOptions).then (data) => 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={}) -> queryOneAttribute: (name, id, attribute, params, options={}) ->
url = @urls.resolve(name, id) url = @urls.resolve(name, id)

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -23,6 +23,57 @@ taiga = @.taiga
module = angular.module("taigaCommon", []) 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 ## Get the selected text
############################################################################# #############################################################################
@ -229,3 +280,46 @@ Template = ($templateCache) ->
} }
module.factory("$tgTemplate", ["$templateCache", Template]) module.factory("$tgTemplate", ["$templateCache", Template])
#############################################################################
## Permission directive, hide elements when necessary
#############################################################################
Capslock = ($translate) ->
link = ($scope, $el, $attrs) ->
open = false
warningIcon = $('<div>')
.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])

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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") title = @translate.instant("ATTACHMENT.TITLE_LIGHTBOX_DELETE_ATTACHMENT")
message = @translate.instant("ATTACHMENT.MSG_LIGHTBOX_DELETE_ATTACHMENT", {fileName: attachment.name}) 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 = => onSuccess = =>
finish() askResponse.finish()
index = @.attachments.indexOf(attachment) index = @.attachments.indexOf(attachment)
@.attachments.splice(index, 1) @.attachments.splice(index, 1)
@.updateCounters() @.updateCounters()
@rootscope.$broadcast("attachment:delete") @rootscope.$broadcast("attachment:delete")
onError = => onError = =>
finish(false) askResponse.finish(false)
message = @translate.instant("ATTACHMENT.ERROR_DELETE_ATTACHMENT", {errorMessage: message}) message = @translate.instant("ATTACHMENT.ERROR_DELETE_ATTACHMENT", {errorMessage: message})
@confirm.notify("error", null, message) @confirm.notify("error", null, message)
return @q.reject() return @q.reject()
@ -197,6 +197,7 @@ AttachmentsDirective = ($config, $confirm, $templates, $translate) ->
$el.on "change", ".attachments-header input", (event) -> $el.on "change", ".attachments-header input", (event) ->
files = _.toArray(event.target.files) files = _.toArray(event.target.files)
return if files.length < 1 return if files.length < 1
$scope.$apply -> $scope.$apply ->
@ -245,7 +246,7 @@ AttachmentsDirective = ($config, $confirm, $templates, $translate) ->
module.directive("tgAttachments", ["$tgConfig", "$tgConfirm", "$tgTemplate", "$translate", AttachmentsDirective]) module.directive("tgAttachments", ["$tgConfig", "$tgConfirm", "$tgTemplate", "$translate", AttachmentsDirective])
AttachmentDirective = ($template, $compile, $translate) -> AttachmentDirective = ($template, $compile, $translate, $rootScope) ->
template = $template.get("attachment/attachment.html", true) template = $template.get("attachment/attachment.html", true)
templateEdit = $template.get("attachment/attachment-edit.html", true) templateEdit = $template.get("attachment/attachment-edit.html", true)
@ -283,6 +284,7 @@ AttachmentDirective = ($template, $compile, $translate) ->
saveAttachment = -> saveAttachment = ->
attachment.description = $el.find("input[name='description']").val() attachment.description = $el.find("input[name='description']").val()
attachment.is_deprecated = $el.find("input[name='is-deprecated']").prop("checked") attachment.is_deprecated = $el.find("input[name='is-deprecated']").prop("checked")
attachment.isCreatedRightNow = false
$scope.$apply -> $scope.$apply ->
$ctrl.updateAttachment(attachment).then -> $ctrl.updateAttachment(attachment).then ->
@ -297,7 +299,7 @@ AttachmentDirective = ($template, $compile, $translate) ->
if event.keyCode == 13 if event.keyCode == 13
saveAttachment() saveAttachment()
else if event.keyCode == 27 else if event.keyCode == 27
render(attachment, false) $scope.$apply -> render(attachment, false)
$el.on "click", "a.editable-settings.icon-delete", (event) -> $el.on "click", "a.editable-settings.icon-delete", (event) ->
event.preventDefault() event.preventDefault()
@ -314,6 +316,12 @@ AttachmentDirective = ($template, $compile, $translate) ->
$scope.$apply -> $scope.$apply ->
$ctrl.removeAttachment(attachment) $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", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -329,4 +337,4 @@ AttachmentDirective = ($template, $compile, $translate) ->
restrict: "AE" restrict: "AE"
} }
module.directive("tgAttachment", ["$tgTemplate", "$compile", "$translate", AttachmentDirective]) module.directive("tgAttachment", ["$tgTemplate", "$compile", "$translate", "$rootScope", AttachmentDirective])

View File

@ -1,3 +1,22 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: bind-scope.coffee
###
module = angular.module("taigaCommon") module = angular.module("taigaCommon")
BindScope = (config) -> BindScope = (config) ->

View File

@ -1,3 +1,22 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: compile-html.directive.coffee
###
CompileHtmlDirective = ($compile) -> CompileHtmlDirective = ($compile) ->
link = (scope, element, attrs) -> link = (scope, element, attrs) ->
scope.$watch attrs.tgCompileHtml, (newValue, oldValue) -> scope.$watch attrs.tgCompileHtml, (newValue, oldValue) ->

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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) ## Date Selector Directive (using pikaday)
############################################################################# #############################################################################
DateSelectorDirective = ($rootscope, $translate) -> DateSelectorDirective = ($rootscope, datePickerConfigService) ->
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
selectedDate = null selectedDate = null
initialize = () -> initialize = () ->
$el.picker = new Pikaday({ datePickerConfig = datePickerConfigService.get()
_.merge(datePickerConfig, {
field: $el[0] field: $el[0]
onSelect: (date) => onSelect: (date) =>
selectedDate = date selectedDate = date
onOpen: => onOpen: =>
$el.picker.setDate(selectedDate) if selectedDate? $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() unbind = $rootscope.$on "$translateChangeEnd", (ctx) => initialize()
$scope.$watch $attrs.ngModel, (val) -> $scope.$watch $attrs.ngModel, (val) ->
@ -113,7 +84,7 @@ DateSelectorDirective = ($rootscope, $translate) ->
require: "ngModel" 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 ## 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 # Display the owner information (full name and photo) and the date of
# creation of an object (like USs, tasks and issues). # creation of an object (like USs, tasks and issues).
# #
@ -168,13 +139,14 @@ CreatedByDisplayDirective = ($template, $compile, $translate)->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
render = (model) -> render = (model) ->
owner = $scope.usersById?[model.owner] or { owner = model.owner_extra_info or {
full_name_display: $translate.instant("COMMON.EXTERNAL_USER") full_name_display: $translate.instant("COMMON.EXTERNAL_USER")
photo: "/images/unnamed.png" photo: "/images/user-noimage.png"
} }
html = template({ html = template({
owner: owner 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")) date: moment(model.created_date).format($translate.instant("COMMON.DATETIME"))
}) })
@ -194,7 +166,8 @@ CreatedByDisplayDirective = ($template, $compile, $translate)->
require: "ngModel" 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) html = $compile(template(ctx))($scope)
$el.html(html) $el.html(html)
if isEditable() and watchers.length == 0 $el.on "click", ".js-delete-watcher", (event) ->
$el.find(".title").text("Add watchers")
$el.find(".watchers-header").addClass("no-watchers")
$el.on "click", ".icon-delete", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable() return if not isEditable()
target = angular.element(event.currentTarget) 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") title = $translate.instant("COMMON.WATCHERS.TITLE_LIGHTBOX_DELETE_WARTCHER")
message = $scope.usersById[watcherId].full_name_display message = $scope.usersById[watcherId].full_name_display
$confirm.askOnDelete(title, message).then (finish) => $confirm.askOnDelete(title, message).then (askResponse) =>
finish() askResponse.finish()
watcherIds = _.clone($model.$modelValue.watchers, false) watcherIds = _.clone($model.$modelValue.watchers, false)
watcherIds = _.pull(watcherIds, watcherId) watcherIds = _.pull(watcherIds, watcherId)
deleteWatcher(watcherIds) deleteWatcher(watcherIds)
$el.on "click", ".add-watcher", (event) -> $el.on "click", ".js-add-watcher", (event) ->
event.preventDefault() event.preventDefault()
return if not isEditable() return if not isEditable()
$scope.$apply -> $scope.$apply ->
@ -353,8 +322,8 @@ AssignedToDirective = ($rootscope, $confirm, $repo, $loading, $qqueue, $template
return if not isEditable() return if not isEditable()
title = $translate.instant("COMMON.ASSIGNED_TO.CONFIRM_UNASSIGNED") title = $translate.instant("COMMON.ASSIGNED_TO.CONFIRM_UNASSIGNED")
$confirm.ask(title).then (finish) => $confirm.ask(title).then (response) =>
finish() response.finish()
$model.$modelValue.assigned_to = null $model.$modelValue.assigned_to = null
save(null) save(null)
@ -443,18 +412,18 @@ DeleteButtonDirective = ($log, $repo, $confirm, $location, $template) ->
if not $attrs.onDeleteTitle if not $attrs.onDeleteTitle
return $log.error "DeleteButtonDirective requires on-delete-title set in scope." 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 title = $attrs.onDeleteTitle
subtitle = $model.$modelValue.subject subtitle = $model.$modelValue.subject
$confirm.askOnDelete(title, subtitle).then (finish) => $confirm.askOnDelete(title, subtitle).then (askResponse) =>
promise = $repo.remove($model.$modelValue) promise = $repo.remove($model.$modelValue)
promise.then => promise.then =>
finish() askResponse.finish()
url = $scope.$eval($attrs.onDeleteGoToUrl) url = $scope.$eval($attrs.onDeleteGoToUrl)
$location.path(url) $location.path(url)
promise.then null, => promise.then null, =>
finish(false) askResponse.finish(false)
$confirm.notify("error") $confirm.notify("error")
$scope.$on "$destroy", -> $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) -> 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('.view-description').hide()
$el.find('textarea').focus() $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) -> $el.on "click", ".save", (e) ->
e.preventDefault() e.preventDefault()
@ -673,14 +649,14 @@ ListItemAssignedtoDirective = ($template) ->
template = $template.get("common/components/list-item-assigned-to-avatar.html", true) template = $template.get("common/components/list-item-assigned-to-avatar.html", true)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
bindOnce $scope, "membersById", (membersById) -> bindOnce $scope, "usersById", (usersById) ->
item = $scope.$eval($attrs.tgListitemAssignedto) item = $scope.$eval($attrs.tgListitemAssignedto)
ctx = {name: "Unassigned", imgurl: "/images/unnamed.png"} ctx = {name: "Unassigned", imgurl: "/images/unnamed.png"}
member = membersById[item.assigned_to] member = usersById[item.assigned_to]
if member if member
ctx.imgurl = member.photo ctx.imgurl = member.photo
ctx.name = member.full_name ctx.name = member.full_name_display
$el.html(template(ctx)) $el.html(template(ctx))

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -67,11 +67,12 @@ class ConfirmService extends taiga.Service
currentLoading = @loading() currentLoading = @loading()
.target(target) .target(target)
.start() .start()
defered.resolve {
defered.resolve (ok=true) => finish: (ok=true) =>
currentLoading.finish() currentLoading.finish()
if ok if ok
@.hide(el) @.hide(el)
}
el.on "click.confirm-dialog", "a.button-red", (event) => el.on "click.confirm-dialog", "a.button-red", (event) =>
event.preventDefault() event.preventDefault()
@ -118,8 +119,9 @@ class ConfirmService extends taiga.Service
.start() .start()
defered.resolve { defered.resolve {
selected: choicesField.val() selected: choicesField.val()
finish: => finish: (ok=true) =>
currentLoading.finish() currentLoading.finish()
if ok
@.hide(el) @.hide(el)
} }
@ -245,7 +247,7 @@ class ConfirmService extends taiga.Service
delete @.tsem delete @.tsem
el.on "click", ".icon-delete", (event) => el.on "click", ".icon-delete, .close", (event) =>
body.find(selector) body.find(selector)
.removeClass('active') .removeClass('active')
.addClass('inactive') .addClass('inactive')

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -27,6 +27,28 @@ generateHash = taiga.generateHash
module = angular.module("taigaCommon") 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 class CustomAttributesValuesController extends taiga.Controller
@.$inject = ["$scope", "$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$q"] @.$inject = ["$scope", "$rootScope", "$tgRepo", "$tgResources", "$tgConfirm", "$q"]
@ -118,32 +140,51 @@ CustomAttributesValuesDirective = ($templates, $storage) ->
template: templateFn 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) template = $template.get("custom-attributes/custom-attribute-value.html", true)
templateEdit = $template.get("custom-attributes/custom-attribute-value-edit.html", true) templateEdit = $template.get("custom-attributes/custom-attribute-value-edit.html", true)
link = ($scope, $el, $attrs, $ctrl) -> link = ($scope, $el, $attrs, $ctrl) ->
prettyDate = $translate.instant("COMMON.PICKERDATE.FORMAT")
render = (attributeValue, edit=false) -> render = (attributeValue, edit=false) ->
if attributeValue.type is DATE_TYPE and attributeValue.value
value = moment(attributeValue.value, "YYYY-MM-DD").format(prettyDate)
else
value = attributeValue.value value = attributeValue.value
editable = isEditable() editable = isEditable()
ctx = { ctx = {
id: attributeValue.id id: attributeValue.id
name: attributeValue.name name: attributeValue.name
description: attributeValue.description description: attributeValue.description
value: value value: value
isEditable: editable isEditable: editable
type: attributeValue.type
} }
if editable and (edit or not value) if editable and (edit or not value)
html = templateEdit(ctx) html = templateEdit(ctx)
html = $compile(html)($scope) 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 else
html = template(ctx) html = template(ctx)
html = $compile(html)($scope) html = $compile(html)($scope)
$el.html(html) $el.html(html)
isEditable = -> isEditable = ->
@ -151,52 +192,58 @@ CustomAttributeValueDirective = ($template, $selectedText, $compile) ->
requiredEditionPerm = $attrs.requiredEditionPerm requiredEditionPerm = $attrs.requiredEditionPerm
return permissions.indexOf(requiredEditionPerm) > -1 return permissions.indexOf(requiredEditionPerm) > -1
saveAttributeValue = -> submit = debounce 2000, (event) =>
attributeValue.value = $el.find("input").val() 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 -> $scope.$apply ->
$ctrl.updateAttributeValue(attributeValue).then -> $ctrl.updateAttributeValue(attributeValue).then ->
render(attributeValue, false) render(attributeValue, false)
$el.on "keyup", "input[name=description]", (event) -> setFocusAndSelectOnInputField = ->
if event.keyCode == 13 $el.find("input[name='value'], textarea[name='value']").focus().select()
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()
# Bootstrap # Bootstrap
attributeValue = $scope.$eval($attrs.tgCustomAttributeValue) attributeValue = $scope.$eval($attrs.tgCustomAttributeValue)
render(attributeValue) 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 { return {
link: link link: link
require: "^tgCustomAttributesValues" require: "^tgCustomAttributesValues"
restrict: "AE" restrict: "AE"
} }
module.directive("tgCustomAttributeValue", ["$tgTemplate", "$selectedText", "$compile", CustomAttributeValueDirective]) module.directive("tgCustomAttributeValue", ["$tgTemplate", "$selectedText", "$compile", "$translate",
"tgDatePickerConfigService", CustomAttributeValueDirective])

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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() @$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) -> create = ($el, us, project) ->
$el.unbind("click") $el.unbind("click")

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -26,6 +26,14 @@ debounce = @.taiga.debounce
module = angular.module("taigaCommon") 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) ## History Directive (Main)
@ -68,7 +76,7 @@ class HistoryController extends taiga.Controller
return @rs.history.undeleteComment(type, objectId, activityId).then => @.loadHistory(type, objectId) 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) templateChangeDiff = $template.get("common/history/history-change-diff.html", true)
templateChangePoints = $template.get("common/history/history-change-points.html", true) templateChangePoints = $template.get("common/history/history-change-points.html", true)
templateChangeGeneric = $template.get("common/history/history-change-generic.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 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) -> countChanges = (comment) ->
return _.keys(comment.values_diff).length 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}) return templateChangeGeneric({name:name, from:from, to: to})
renderChangeEntries = (change) -> 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)) return _.map(change.values_diff, (value, field) -> renderChangeEntry(field, value))
renderChangesHelperText = (change) -> renderChangesHelperText = (change) ->
@ -286,8 +289,9 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c
return html[0].outerHTML return html[0].outerHTML
html = templateActivity({ html = templateActivity({
avatar: getUserAvatar(comment.user.pk) avatar: comment.user.photo
userFullName: comment.user.name 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()) creationDate: moment(comment.created_at).format(getPrettyDateFormat())
comment: comment.comment_html comment: comment.comment_html
changesText: renderChangesHelperText(comment) changesText: renderChangesHelperText(comment)
@ -305,8 +309,9 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c
renderChange = (change) -> renderChange = (change) ->
return templateActivity({ return templateActivity({
avatar: getUserAvatar(change.user.pk) avatar: change.user.photo
userFullName: change.user.name 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()) creationDate: moment(change.created_at).format(getPrettyDateFormat())
comment: change.comment_html comment: change.comment_html
changes: renderChangeEntries(change) changes: renderChangeEntries(change)
@ -359,6 +364,8 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c
.start() .start()
onSuccess = -> onSuccess = ->
$rootScope.$broadcast("comment:new")
$ctrl.loadHistory(type, objectId).finally -> $ctrl.loadHistory(type, objectId).finally ->
currentLoading.finish() currentLoading.finish()
@ -385,6 +392,13 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
save(target) 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) -> $el.on "click", ".show-more", (event) ->
event.preventDefault() event.preventDefault()
@ -419,8 +433,13 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c
$(this).addClass('active') $(this).addClass('active')
$el.on "click", ".history-tabs li a", (event) -> $el.on "click", ".history-tabs li a", (event) ->
$el.find(".history-tabs li a").toggleClass("active") target = angular.element(event.currentTarget)
$el.find(".history section").toggleClass("hidden")
$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) -> $el.on "click", ".comment-delete", debounce 2000, (event) ->
event.preventDefault() event.preventDefault()
@ -454,4 +473,4 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c
module.directive("tgHistory", ["$log", "$tgLoading", "$tgQqueue", "$tgTemplate", "$tgConfirm", "$translate", module.directive("tgHistory", ["$log", "$tgLoading", "$tgQqueue", "$tgTemplate", "$tgConfirm", "$translate",
"$compile", HistoryDirective]) "$compile", "$tgNavUrls", "$rootScope", HistoryDirective])

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -39,9 +39,10 @@ class LightboxService extends taiga.Service
lightboxContent = $el.children().not(".close") lightboxContent = $el.children().not(".close")
lightboxContent.hide() lightboxContent.hide()
@animationFrame.add ->
$el.css('display', 'flex') $el.css('display', 'flex')
@animationFrame.add => @animationFrame.add ->
$el.addClass("open") $el.addClass("open")
@animationFrame.add -> @animationFrame.add ->
@ -475,10 +476,9 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic
} }
html = usersTemplate(ctx) html = usersTemplate(ctx)
html = $compile(html)($scope) html = $compile(html)($scope)
$el.find("div.watchers").html(html) $el.find(".assigned-to-list").html(html)
closeLightbox = () -> closeLightbox = () ->
lightboxKeyboardNavigationService.stop() lightboxKeyboardNavigationService.stop()
@ -499,7 +499,7 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic
render(selectedUser, searchingText) render(selectedUser, searchingText)
$el.find('input').focus() $el.find('input').focus()
$el.on "click", ".watcher-single", (event) -> $el.on "click", ".user-list-single", (event) ->
event.preventDefault() event.preventDefault()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
@ -543,7 +543,7 @@ module.directive("tgLbAssignedto", ["lightboxService", "lightboxKeyboardNavigati
## Watchers Lightbox directive ## Watchers Lightbox directive
############################################################################# #############################################################################
WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template) -> WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationService, $template, $compile) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
selectedItem = null selectedItem = null
usersTemplate = $template.get("common/lightbox/lightbox-assigned-to-users.html", true) usersTemplate = $template.get("common/lightbox/lightbox-assigned-to-users.html", true)
@ -572,7 +572,8 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS
} }
html = usersTemplate(ctx) html = usersTemplate(ctx)
$el.find("div.watchers").html(html) html = $compile(html)($scope)
$el.find(".ticket-watchers").html(html)
closeLightbox = () -> closeLightbox = () ->
lightboxKeyboardNavigationService.stop() lightboxKeyboardNavigationService.stop()
@ -596,7 +597,7 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS
render(users) render(users)
$el.find("input").focus() $el.find("input").focus()
$el.on "click", ".watcher-single", debounce 2000, (event) -> $el.on "click", ".user-list-single", debounce 2000, (event) ->
closeLightbox() closeLightbox()
event.preventDefault() event.preventDefault()
@ -622,4 +623,37 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS
link:link 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])

View File

@ -1,9 +1,9 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# Copyright (C) 2014 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net> # Copyright (C) 2014-2015 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014 Alejandro Alonso <alejandro.alonso@kaleidos.net> # Copyright (C) 2014-2015 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -77,19 +77,11 @@ Loader = ($rootscope) ->
lastResponseDate = 0 lastResponseDate = 0
autoClose = () -> autoClose = () ->
maxAuto = 5000
timeoutAuto = setTimeout (() ->
pageLoaded()
clearInterval(intervalAuto)
), maxAuto
intervalAuto = setInterval (() -> intervalAuto = setInterval (() ->
if lastResponseDate && requestCount == 0 if lastResponseDate && requestCount == 0
pageLoaded() pageLoaded()
clearInterval(intervalAuto) clearInterval(intervalAuto)
clearTimeout(timeoutAuto)
), 50 ), 50
start = () -> start = () ->

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -21,19 +21,29 @@
module = angular.module("taigaCommon") module = angular.module("taigaCommon")
TgLoadingService = -> TgLoadingService = ($compile) ->
spinner = "<img class='loading-spinner' src='/svg/spinner-circle.svg' alt='loading...' />" spinner = "<img class='loading-spinner' src='/svg/spinner-circle.svg' alt='loading...' />"
return () -> return () ->
service = { service = {
settings: { settings: {
target: null, target: null,
scope: null,
classes: [] classes: []
timeout: 0 timeout: 0,
template: null
}, },
target: (target) -> target: (target) ->
service.settings.target = target service.settings.target = target
return service
scope: (scope) ->
service.settings.scope = scope
return service
template: (template) ->
service.settings.template = template
return service return service
removeClasses: (classess...) -> removeClasses: (classess...) ->
service.settings.classes = classess service.settings.classes = classess
@ -51,9 +61,11 @@ TgLoadingService = ->
# The loader is shown after that quantity of milliseconds # The loader is shown after that quantity of milliseconds
timeoutId = setTimeout (-> timeoutId = setTimeout (->
if not target.hasClass('loading') if not target.hasClass('loading')
service.settings.oldContent = target.html() if !service.settings.template
service.settings.template = target.html()
target.addClass('loading') target.addClass('loading')
target.html(spinner) target.html(spinner)
), service.settings.timeout ), service.settings.timeout
@ -71,27 +83,37 @@ TgLoadingService = ->
removeClasses = service.settings.classes removeClasses = service.settings.classes
removeClasses.map (className) -> service.settings.target.addClass(className) removeClasses.map (className) -> service.settings.target.addClass(className)
target.html(service.settings.oldContent) target.html(service.settings.template)
target.removeClass('loading') target.removeClass('loading')
if service.settings.scope
$compile(target.contents())(service.settings.scope)
return service return service
} }
return service return service
TgLoadingService.$inject = [
"$compile"
]
module.factory("$tgLoading", TgLoadingService) module.factory("$tgLoading", TgLoadingService)
LoadingDirective = ($loading) -> LoadingDirective = ($loading) ->
link = ($scope, $el, attr) -> link = ($scope, $el, attr) ->
currentLoading = null currentLoading = null
template = $el.html()
$scope.$watch attr.tgLoading, (showLoading) => $scope.$watch attr.tgLoading, (showLoading) =>
if showLoading if showLoading
currentLoading = $loading() currentLoading = $loading()
.target($el) .target($el)
.timeout(50)
.template(template)
.scope($scope)
.start() .start()
else else if currentLoading
currentLoading.finish() currentLoading.finish()
return { return {

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 startIndex = result.index
break break
return if !result
regex = />>>/gi regex = />>>/gi
endIndex = 0 endIndex = 0
loop loop
@ -181,10 +183,19 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans
onShiftEnter: {keepDefault:false, openWith:"\n\n"} onShiftEnter: {keepDefault:false, openWith:"\n\n"}
onEnter: onEnter:
keepDefault: false, 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) -> afterInsert: (data) ->
lines = data.textarea.value.split("\n") lines = data.textarea.value.split("\n")
# 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 cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length
else
cursorLine = 1
newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0] newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0]
lastLine = lines[cursorLine - 1] lastLine = lines[cursorLine - 1]
@ -342,6 +353,104 @@ MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $trans
element element
.markItUpRemove() .markItUpRemove()
.markItUp(markdownSettings) .markItUp(markdownSettings)
.textcomplete([
# us, task, and issue autocomplete: #id or #<part of title>
{
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 @<part of name>
{
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 [[<part of slug>
# 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() renderMarkItUp()

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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.usersById = groupBy(@scope.users, (e) -> e.id)
@scope.roles = _.sortBy(roles, "order") @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") @scope.computableRoles = _(roles).filter("computable")
.filter((x) -> _.contains(availableRoles, x.id)) .filter((x) -> _.contains(computableRoles, x.id))
.value() .value()
loadUsersAndRoles: -> loadUsersAndRoles: ->
promise = @q.all([ promise = @q.all([

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -26,6 +26,7 @@ toString = @.taiga.toString
joinStr = @.taiga.joinStr joinStr = @.taiga.joinStr
groupBy = @.taiga.groupBy groupBy = @.taiga.groupBy
bindOnce = @.taiga.bindOnce bindOnce = @.taiga.bindOnce
bindMethods = @.taiga.bindMethods
module = angular.module("taigaIssues") 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, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
@log, @appMetaService, @analytics, @navUrls, @translate) -> @log, @appMetaService, @analytics, @navUrls, @translate) ->
bindMethods(@)
@scope.issueRef = @params.issueref @scope.issueRef = @params.issueref
@scope.sectionName = @translate.instant("ISSUES.SECTION_NAME") @scope.sectionName = @translate.instant("ISSUES.SECTION_NAME")
@.initializeEventHandlers() @.initializeEventHandlers()
@ -83,20 +86,16 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "attachment:create", => @scope.$on "attachment:create", =>
@rootscope.$broadcast("object:updated")
@analytics.trackEvent("attachment", "create", "create attachment on issue", 1) @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", => @scope.$on "promote-issue-to-us:success", =>
@analytics.trackEvent("issue", "promoteToUserstory", "promote issue to userstory", 1) @analytics.trackEvent("issue", "promoteToUserstory", "promote issue to userstory", 1)
@rootscope.$broadcast("object:updated") @rootscope.$broadcast("object:updated")
@.loadIssue() @.loadIssue()
@scope.$on "comment:new", =>
@.loadIssue()
@scope.$on "custom-attributes-values:edit", => @scope.$on "custom-attributes-values:edit", =>
@rootscope.$broadcast("object:updated") @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.severityById = groupBy(project.severities, (x) -> x.id)
@scope.priorityList = project.priorities @scope.priorityList = project.priorities
@scope.priorityById = groupBy(project.priorities, (x) -> x.id) @scope.priorityById = groupBy(project.priorities, (x) -> x.id)
@scope.membersById = groupBy(project.memberships, (x) -> x.user)
return project return project
loadIssue: -> loadIssue: ->
@ -146,9 +144,52 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
loadInitialData: -> loadInitialData: ->
promise = @.loadProject() promise = @.loadProject()
return promise.then (project) => return promise.then (project) =>
@.fillUsersAndRoles(project.users, project.roles) @.fillUsersAndRoles(project.members, project.roles)
@.loadIssue() @.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) module.controller("IssueDetailController", IssueDetailController)
@ -558,7 +599,7 @@ module.directive("tgIssuePriorityButton", ["$rootScope", "$tgRepo", "$tgConfirm"
PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm, $qqueue, $translate) -> PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm, $qqueue, $translate) ->
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
save = $qqueue.bindAdd (issue, finish) => save = $qqueue.bindAdd (issue, askResponse) =>
data = { data = {
generated_from_issue: issue.id generated_from_issue: issue.id
project: issue.project, project: issue.project,
@ -570,12 +611,12 @@ PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm, $qqueue, $transl
} }
onSuccess = -> onSuccess = ->
finish() askResponse.finish()
$confirm.notify("success") $confirm.notify("success")
$rootScope.$broadcast("promote-issue-to-us:success") $rootScope.$broadcast("promote-issue-to-us:success")
onError = -> onError = ->
finish(false) askResponse.finish()
$confirm.notify("error") $confirm.notify("error")
$repo.create("userstories", data).then(onSuccess, onError) $repo.create("userstories", data).then(onSuccess, onError)
@ -589,9 +630,8 @@ PromoteIssueToUsButtonDirective = ($rootScope, $repo, $confirm, $qqueue, $transl
message = $translate.instant("ISSUES.CONFIRM_PROMOTE.MESSAGE") message = $translate.instant("ISSUES.CONFIRM_PROMOTE.MESSAGE")
subtitle = issue.subject subtitle = issue.subject
$confirm.ask(title, subtitle, message).then (finish) => $confirm.ask(title, subtitle, message).then (response) =>
save(issue, finish) save(issue, response)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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", => @scope.$on "issueform:new:success", =>
@analytics.trackEvent("issue", "create", "create issue on issues list", 1) @analytics.trackEvent("issue", "create", "create issue on issues list", 1)
@.loadIssues() @.loadIssues()
@.loadFilters()
initializeSubscription: -> initializeSubscription: ->
routingKey = "changes.project.#{@scope.projectId}.issues" 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.issueTypes = _.sortBy(project.issue_types, "order")
@scope.issueTypeById = groupBy(project.issue_types, (x) -> x.id) @scope.issueTypeById = groupBy(project.issue_types, (x) -> x.id)
@scope.membersById = groupBy(project.memberships, (x) -> x.user)
return project return project
getUrlFilters: -> getUrlFilters: ->
filters = _.pick(@location.search(), "page", "tags", "statuses", "types", filters = _.pick(@location.search(), "page", "tags", "status", "types",
"q", "severities", "priorities", "q", "severities", "priorities",
"assignedTo", "createdBy", "orderBy") "assignedTo", "createdBy", "orderBy")
filters.page = 1 if not filters.page filters.page = 1 if not filters.page
return filters return filters
@ -181,20 +179,30 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.filters.myFilters = myFilters @scope.filters.myFilters = myFilters
return 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 # Load default filters data
promise = promise.then => promise = promise.then =>
return @rs.issues.filtersData(@scope.projectId) return @rs.issues.filtersData(loadFilters)
# Format filters and set them on scope # Format filters and set them on scope
return promise.then (data) => return promise.then (data) =>
usersFiltersFormat = (users, type, unknownOption) => usersFiltersFormat = (users, type, unknownOption) =>
reformatedUsers = _.map users, (t) => reformatedUsers = _.map users, (t) =>
return { t.type = type
id: t[0], t.name = if t.full_name then t.full_name else unknownOption
count: t[1],
type: type return t
name: if t[0] then @scope.usersById[t[0]].full_name_display else unknownOption
}
unknownItem = _.remove(reformatedUsers, (u) -> not u.id) unknownItem = _.remove(reformatedUsers, (u) -> not u.id)
reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase()) reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase())
if unknownItem.length > 0 if unknownItem.length > 0
@ -203,34 +211,27 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
choicesFiltersFormat = (choices, type, byIdObject) => choicesFiltersFormat = (choices, type, byIdObject) =>
_.map choices, (t) -> _.map choices, (t) ->
return { t.type = type
id: t[0], return t
name: byIdObject[t[0]].name,
color: byIdObject[t[0]].color,
count: t[1],
type: type}
tagsFilterFormat = (tags) => tagsFilterFormat = (tags) =>
return _.map tags, (t) => return _.map tags, (t) ->
return { t.id = t.name
id: t[0], t.type = 'tags'
name: t[0], return t
color: @scope.project.tags_colors[t[0]],
count: t[1],
type: "tags"
}
# Build filters data structure # 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.severities = choicesFiltersFormat(data.severities, "severities", @scope.severityById)
@scope.filters.priorities = choicesFiltersFormat(data.priorities, "priorities", @scope.priorityById) @scope.filters.priorities = choicesFiltersFormat(data.priorities, "priorities", @scope.priorityById)
@scope.filters.assignedTo = usersFiltersFormat(data.assigned_to, "assignedTo", "Unassigned") @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.types = choicesFiltersFormat(data.types, "types", @scope.issueTypeById)
@scope.filters.tags = tagsFilterFormat(data.tags) @scope.filters.tags = tagsFilterFormat(data.tags)
@.removeNotExistingFiltersFromUrl() @.removeNotExistingFiltersFromUrl()
@.markSelectedFilters(@scope.filters, urlfilters) @.markSelectedFilters(@scope.filters, urlfilters)
@rootscope.$broadcast("filters:loaded", @scope.filters) @rootscope.$broadcast("filters:loaded", @scope.filters)
# We need to guarantee that the last petition done here is the finally used # 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" name = "assigned_to"
else if name == "createdBy" else if name == "createdBy"
name = "owner" name = "owner"
else if name == "statuses" else if name == "status"
name = "status" name = "status"
else if name == "types" else if name == "types"
name = "type" name = "type"
@ -273,14 +274,19 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.page = data.current @scope.page = data.current
@scope.count = data.count @scope.count = data.count
@scope.paginatedBy = data.paginatedBy @scope.paginatedBy = data.paginatedBy
return data return data
return promise
loadInitialData: -> loadInitialData: ->
promise = @.loadProject() promise = @.loadProject()
return promise.then (project) => return promise.then (project) =>
@.fillUsersAndRoles(project.users, project.roles) @.fillUsersAndRoles(project.members, project.roles)
@.initializeSubscription() @.initializeSubscription()
return @q.all([@.loadFilters(), @.loadIssues()]) @.loadFilters()
return @.loadIssues()
saveCurrentFiltersTo: (newFilter) -> saveCurrentFiltersTo: (newFilter) ->
deferred = @q.defer() deferred = @q.defer()
@ -401,7 +407,7 @@ IssuesDirective = ($log, $location, $template, $compile) ->
# Draw the arrow the first time # Draw the arrow the first time
currentOrder = $ctrl.getUrlFilter("orderBy") or "created_date" currentOrder = $ctrl.getUrlFilter("orderBy") or "created_date"
if currentOrder 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 = $el.find(".row.title > div[data-fieldname='#{trim(currentOrder, "-")}']")
colHeadElement.html("#{colHeadElement.html()}<span class='icon #{icon}'></span>") colHeadElement.html("#{colHeadElement.html()}<span class='icon #{icon}'></span>")
@ -419,7 +425,7 @@ IssuesDirective = ($log, $location, $template, $compile) ->
$ctrl.loadIssues().then -> $ctrl.loadIssues().then ->
# Update the arrow # Update the arrow
$el.find(".row.title > div > span.icon").remove() $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()}<span class='icon #{icon}'></span>") target.html("#{target.html()}<span class='icon #{icon}'></span>")
## Issues Link ## Issues Link
@ -440,12 +446,13 @@ module.directive("tgIssues", ["$log", "$tgLocation", "$tgTemplate", "$compile",
## Issues Filters Directive ## 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) template = $template.get("issue/issues-filters.html", true)
templateSelected = $template.get("issue/issues-filters-selected.html", true) templateSelected = $template.get("issue/issues-filters-selected.html", true)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$ctrl = $el.closest(".wrapper").controller() $ctrl = $el.closest(".wrapper").controller()
selectedFilters = [] selectedFilters = []
showFilters = (title, type) -> showFilters = (title, type) ->
@ -491,6 +498,16 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $
html = $compile(html)($scope) html = $compile(html)($scope)
$el.find(".filter-list").html(html) $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) -> toggleFilterSelection = (type, id) ->
if type == "myFilters" if type == "myFilters"
$rs.issues.getMyFilters($scope.projectId).then (data) -> $rs.issues.getMyFilters($scope.projectId).then (data) ->
@ -507,7 +524,6 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $
filters = $scope.filters[type] filters = $scope.filters[type]
filterId = if type == 'tags' then taiga.toString(id) else id filterId = if type == 'tags' then taiga.toString(id) else id
filter = _.find(filters, {id: filterId}) filter = _.find(filters, {id: filterId})
filter.selected = (not filter.selected) filter.selected = (not filter.selected)
# Convert id to null as string for properly # Convert id to null as string for properly
@ -516,22 +532,23 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $
if filter.selected if filter.selected
selectedFilters.push(filter) selectedFilters.push(filter)
$scope.$apply ->
$ctrl.selectFilter(type, id) $ctrl.selectFilter(type, id)
$ctrl.selectFilter("page", 1) $ctrl.selectFilter("page", 1)
$ctrl.storeFilters() $ctrl.storeFilters()
$ctrl.loadIssues()
else else
selectedFilters = _.reject(selectedFilters, filter) selectedFilters = _.reject selectedFilters, (f) ->
$scope.$apply -> return f.id == filter.id && f.type == filter.type
$ctrl.unselectFilter(type, id) $ctrl.unselectFilter(type, id)
$ctrl.selectFilter("page", 1) $ctrl.selectFilter("page", 1)
$ctrl.storeFilters() $ctrl.storeFilters()
$ctrl.loadIssues()
reloadIssues()
renderSelectedFilters(selectedFilters) renderSelectedFilters(selectedFilters)
currentFiltersType = $el.find("h2 a.subfilter span.title").prop('data-type') currentFiltersType = getFiltersType()
if type == currentFiltersType if type == currentFiltersType
renderFilters(_.reject(filters, "selected")) renderFilters(_.reject(filters, "selected"))
@ -540,7 +557,7 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $
initializeSelectedFilters(filters) initializeSelectedFilters(filters)
$scope.$on "filters:issueupdate", (ctx, filters) -> $scope.$on "filters:issueupdate", (ctx, filters) ->
html = template({filters:filters.statuses}) html = template({filters:filters.status})
html = $compile(html)($scope) html = $compile(html)($scope)
$el.find(".filter-list").html(html) $el.find(".filter-list").html(html)
@ -555,7 +572,8 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $
else else
$ctrl.replaceFilter("q", value) $ctrl.replaceFilter("q", value)
$ctrl.storeFilters() $ctrl.storeFilters()
$ctrl.loadIssues()
reloadIssues()
$scope.$watch("filtersQ", selectQFilter) $scope.$watch("filtersQ", selectQFilter)
@ -602,18 +620,18 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $
title = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.TITLE") title = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.TITLE")
message = $translate.instant("ISSUES.FILTERS.CONFIRM_DELETE.MESSAGE", {customFilterName: customFilterName}) 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 = $ctrl.deleteMyFilter(customFilterName)
promise.then -> promise.then ->
promise = $ctrl.loadMyFilters() promise = $ctrl.loadMyFilters()
promise.then (filters) -> promise.then (filters) ->
finish() askResponse.finish()
$scope.filters.myFilters = filters $scope.filters.myFilters = filters
renderFilters($scope.filters.myFilters) renderFilters($scope.filters.myFilters)
promise.then null, -> promise.then null, ->
finish() askResponse.finish()
promise.then null, -> promise.then null, ->
finish(false) askResponse.finish(false)
$confirm.notify("error") $confirm.notify("error")
@ -664,7 +682,7 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $
return {link:link} 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]) "$tgTemplate", "$translate", "$compile", "$tgAuth", IssuesFiltersDirective])
@ -711,7 +729,7 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) ->
event.stopPropagation() event.stopPropagation()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
for filter in $scope.filters.statuses for filter in $scope.filters.status
if filter.id == issue.status if filter.id == issue.status
filter.count-- filter.count--
@ -723,17 +741,12 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) ->
$repo.save(issue).then -> $repo.save(issue).then ->
$ctrl.loadIssues() $ctrl.loadIssues()
for filter in $scope.filters.statuses for filter in $scope.filters.status
if filter.id == issue.status if filter.id == issue.status
filter.count++ filter.count++
$rootscope.$broadcast("filters:issueupdate", $scope.filters) $rootscope.$broadcast("filters:issueupdate", $scope.filters)
for filter in $scope.filters.statuses
if filter.id == issue.status
filter.count++
$rootscope.$broadcast("filters:issueupdate", $scope.filters)
taiga.bindOnce $scope, "project", (project) -> taiga.bindOnce $scope, "project", (project) ->
$el.append(selectionTemplate({ 'statuses': project.issue_statuses })) $el.append(selectionTemplate({ 'statuses': project.issue_statuses }))
updateIssueStatus($el, issue, $scope.issueStatusById) updateIssueStatus($el, issue, $scope.issueStatusById)

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -34,14 +34,10 @@ module = angular.module("taigaKanban")
# Vars # Vars
defaultViewMode = "maximized" defaultViewMode = "maximized"
defaultViewModes = { viewModes = [
maximized: { "maximized",
cardClass: "kanban-task-maximized" "minimized"
} ]
minimized: {
cardClass: "kanban-task-minimized"
}
}
############################################################################# #############################################################################
@ -157,6 +153,10 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
usByStatus[status.id] = _.sortBy(usByStatus[status.id], "kanban_order") 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 @scope.usByStatus = usByStatus
# The broadcast must be executed when the DOM has been fully reloaded. # 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: -> loadInitialData: ->
promise = @.loadProject() promise = @.loadProject()
return promise.then (project) => return promise.then (project) =>
@.fillUsersAndRoles(project.users, project.roles) @.fillUsersAndRoles(project.members, project.roles)
@.initializeSubscription() @.initializeSubscription()
@.loadKanban().then( => @scope.$broadcast("redraw:wip")) @.loadKanban().then( => @scope.$broadcast("redraw:wip"))
@ -221,8 +221,9 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.statusViewModes = {} @scope.statusViewModes = {}
for status in @scope.usStatusList for status in @scope.usStatusList
mode = storedStatusViewModes[status.id] mode = storedStatusViewModes[status.id] || defaultViewMode
@scope.statusViewModes[status.id] = if _.has(defaultViewModes, mode) then mode else defaultViewMode
@scope.statusViewModes[status.id] = mode
@.storeStatusViewModes() @.storeStatusViewModes()
@ -233,9 +234,13 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.statusViewModes[statusId] = newViewMode @scope.statusViewModes[statusId] = newViewMode
@.storeStatusViewModes() @.storeStatusViewModes()
getCardClass: (statusId)-> isMaximized: (statusId) ->
mode = @scope.statusViewModes[statusId] or defaultViewMode 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 # Utils methods
@ -317,7 +322,7 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader) status = $scope.$eval($attrs.tgKanbanArchivedStatusHeader)
hidden = true hidden = true
$scope.class = "icon icon-open-eye" $scope.class = "icon-open-eye"
$scope.title = showArchivedText $scope.title = showArchivedText
$el.on "click", (event) -> $el.on "click", (event) ->
@ -325,12 +330,12 @@ KanbanArchivedStatusHeaderDirective = ($rootscope, $translate) ->
$scope.$apply -> $scope.$apply ->
if hidden if hidden
$scope.class = "icon icon-open-eye" $scope.class = "icon-open-eye"
$scope.title = showArchivedText $scope.title = showArchivedText
$rootscope.$broadcast("kanban:hide-userstories-for-status", status.id) $rootscope.$broadcast("kanban:hide-userstories-for-status", status.id)
else else
$scope.class = "icon icon-closed-eye" $scope.class = "icon-closed-eye"
$scope.title = hideArchivedText $scope.title = hideArchivedText
$rootscope.$broadcast("kanban:show-userstories-for-status", status.id) $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") else if not us.is_blocked and $el.hasClass("blocked")
$el.removeClass("blocked") $el.removeClass("blocked")
$el.find(".icon-edit").on "click", (event) -> $el.on 'click', '.icon-edit', (event) ->
if $el.find(".icon-edit").hasClass("noclick") if $el.find(".icon-edit").hasClass("noclick")
return return
@ -431,11 +436,17 @@ KanbanUserstoryDirective = ($rootscope, $loading, $rs) ->
$rootscope.$broadcast("usform:edit", editingUserStory) $rootscope.$broadcast("usform:edit", editingUserStory)
currentLoading.finish() currentLoading.finish()
$scope.getTemplateUrl = () ->
if $scope.us.isPlaceholder
return "common/components/kanban-placeholder.html"
else
return "kanban/kanban-task.html"
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
return { return {
templateUrl: "kanban/kanban-task.html" template: '<ng-include src="getTemplateUrl()"/>',
link: link link: link
require: "ngModel" require: "ngModel"
} }

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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)) $location.url($projectUrl.get(response))
lightboxService.close($el) lightboxService.close($el)
currentUserService._loadProjects() currentUserService.loadProjects()
onErrorSubmit = (response) -> onErrorSubmit = (response) ->
currentLoading.finish() currentLoading.finish()
@ -74,10 +74,7 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project
promise.then(onSuccessSubmit, onErrorSubmit) promise.then(onSuccessSubmit, onErrorSubmit)
openLightbox = -> openLightbox = ->
$scope.data = { $scope.data = {}
total_story_points: 100
total_milestones: 5
}
if !$scope.templates.length if !$scope.templates.length
$rs.projects.templates().then (result) => $rs.projects.templates().then (result) =>
@ -173,7 +170,7 @@ DeleteProjectDirective = ($repo, $rootscope, $auth, $location, $navUrls, $confir
$rootscope.$broadcast("projects:reload") $rootscope.$broadcast("projects:reload")
$location.path($navUrls.resolve("home")) $location.path($navUrls.resolve("home"))
$confirm.notify("success") $confirm.notify("success")
currentUserService._loadProjects() currentUserService.loadProjects()
# FIXME: error handling? # FIXME: error handling?
promise.then null, -> promise.then null, ->

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 task = $model.$modelValue
message = task.subject message = task.subject
$confirm.askOnDelete(title, message).then (finish) -> $confirm.askOnDelete(title, message).then (askResponse) ->
promise = $repo.remove(task) promise = $repo.remove(task)
promise.then -> promise.then ->
finish() askResponse.finish()
$confirm.notify("success") $confirm.notify("success")
$scope.$emit("related-tasks:delete") $scope.$emit("related-tasks:delete")
promise.then null, -> promise.then null, ->
askResponse.finish(false)
$confirm.notify("error") $confirm.notify("error")
$scope.$watch $attrs.ngModel, (val) -> $scope.$watch $attrs.ngModel, (val) ->
@ -216,7 +217,7 @@ RelatedTasksDirective = ($repo, $rs, $rootscope) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
loadTasks = -> loadTasks = ->
return $rs.tasks.list($scope.projectId, null, $scope.usId).then (tasks) => return $rs.tasks.list($scope.projectId, null, $scope.usId).then (tasks) =>
$scope.tasks = tasks $scope.tasks = _.sortBy(tasks, 'ref')
return tasks return tasks
$scope.$on "related-tasks:add", -> $scope.$on "related-tasks:add", ->

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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-password": "/users/change_password"
"users-change-email": "/users/change_email" "users-change-email": "/users/change_email"
"users-cancel-account": "/users/cancel" "users-cancel-account": "/users/cancel"
"contacts": "/users/%s/contacts" "user-stats": "/users/%s/stats"
"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 # User - Notification
"permissions": "/permissions" "permissions": "/permissions"
@ -63,6 +66,10 @@ urls = {
"project-templates": "/project-templates" "project-templates": "/project-templates"
"project-modules": "/projects/%s/modules" "project-modules": "/projects/%s/modules"
"bulk-update-projects-order": "/projects/bulk_update_order" "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 # Project Values - Choises
"userstory-statuses": "/userstory-statuses" "userstory-statuses": "/userstory-statuses"
@ -82,15 +89,29 @@ urls = {
"bulk-update-us-backlog-order": "/userstories/bulk_update_backlog_order" "bulk-update-us-backlog-order": "/userstories/bulk_update_backlog_order"
"bulk-update-us-sprint-order": "/userstories/bulk_update_sprint_order" "bulk-update-us-sprint-order": "/userstories/bulk_update_sprint_order"
"bulk-update-us-kanban-order": "/userstories/bulk_update_kanban_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": "/tasks" "tasks": "/tasks"
"bulk-create-tasks": "/tasks/bulk_create" "bulk-create-tasks": "/tasks/bulk_create"
"bulk-update-task-taskboard-order": "/tasks/bulk_update_taskboard_order" "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": "/issues" "issues": "/issues"
"bulk-create-issues": "/issues/bulk_create" "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 pages
"wiki": "/wiki" "wiki": "/wiki"
@ -147,6 +168,10 @@ urls = {
# locales # locales
"locales": "/locales" "locales": "/locales"
# Application tokens
"applications": "/applications"
"application-tokens": "/application-tokens"
} }
# Initialize api urls service # Initialize api urls service

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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} params = {project_id: projectId, bulk_issues: data}
return $http.post(url, params) 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) -> service.stats = (projectId) ->
return $repo.queryOneRaw("projects", "#{projectId}/issues_stats") return $repo.queryOneRaw("projects", "#{projectId}/issues_stats")
service.filtersData = (projectId) -> service.filtersData = (params) ->
return $repo.queryOneRaw("projects", "#{projectId}/issue_filters_data") return $repo.queryOneRaw("issues-filters", null, params)
service.listValues = (projectId, type) -> service.listValues = (projectId, type) ->
params = {"project": projectId} params = {"project": projectId}

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,3 +1,22 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: modules.coffee
###
resourceProvider = ($repo) -> resourceProvider = ($repo) ->
service = {} service = {}

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -39,12 +39,20 @@ resourceProvider = ($repo, $model, $storage) ->
service.list = (projectId, filters) -> service.list = (projectId, filters) ->
params = {"project": projectId} params = {"project": projectId}
params = _.extend({}, params, filters or {}) 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 for m in milestones
uses = m.user_stories uses = m.user_stories
uses = _.map(uses, (u) => $model.make_model("userstories", u)) uses = _.map(uses, (u) => $model.make_model("userstories", u))
m._attrs.user_stories = uses 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) -> return (instance) ->

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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 $http.post(url, params).then (result) ->
return result.data 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) -> service.bulkUpdateTaskTaskboardOrder = (projectId, data) ->
url = $urls.resolve("bulk-update-task-taskboard-order") url = $urls.resolve("bulk-update-task-taskboard-order")
params = {project_id: projectId, bulk_tasks: data} params = {project_id: projectId, bulk_tasks: data}

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -28,7 +28,7 @@ resourceProvider = ($http, $urls) ->
service = {} service = {}
service.contacts = (userId, options={}) -> service.contacts = (userId, options={}) ->
url = $urls.resolve("contacts", userId) url = $urls.resolve("user-contacts", userId)
httpOptions = {headers: {}} httpOptions = {headers: {}}
if not options.enablePagination if not options.enablePagination

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -41,6 +41,9 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
service.listInAllProjects = (filters) -> service.listInAllProjects = (filters) ->
return $repo.queryMany("userstories", filters) return $repo.queryMany("userstories", filters)
service.filtersData = (params) ->
return $repo.queryOneRaw("userstories-filters", null, params)
service.listUnassigned = (projectId, filters) -> service.listUnassigned = (projectId, filters) ->
params = {"project": projectId, "milestone": "null"} params = {"project": projectId, "milestone": "null"}
params = _.extend({}, params, filters or {}) params = _.extend({}, params, filters or {})
@ -64,6 +67,22 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
return $http.post(url, data) 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) -> service.bulkUpdateBacklogOrder = (projectId, data) ->
url = $urls.resolve("bulk-update-us-backlog-order") url = $urls.resolve("bulk-update-us-backlog-order")
params = {project_id: projectId, bulk_stories: data} params = {project_id: projectId, bulk_stories: data}

View File

@ -1,3 +1,22 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: webhooklogs.coffee
###
resourceProvider = ($repo, $urls, $http) -> resourceProvider = ($repo, $urls, $http) ->
service = {} service = {}

View File

@ -1,3 +1,22 @@
###
# Copyright (C) 2014-2015 Taiga Agile LLC <taiga@taiga.io>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: webhooks.coffee
###
resourceProvider = ($repo, $urls, $http) -> resourceProvider = ($repo, $urls, $http) ->
service = {} service = {}

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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(@) promise.then null, @.onInitialDataError.bind(@)
# Search input watcher # Search input watcher
@scope.searchTerm = "" @scope.searchTerm = null
loadSearchData = debounceLeading(100, (t) => @.loadSearchData(t)) loadSearchData = debounceLeading(100, (t) => @.loadSearchData(t))
bindOnce @scope, "projectId", (projectId) =>
if !@scope.searchResults && @scope.searchTerm
@.loadSearchData()
@scope.$watch "searchTerm", (term) => @scope.$watch "searchTerm", (term) =>
if term if term != undefined && @scope.projectId
loadSearchData(term) @.loadSearchData(term)
loadFilters: -> loadFilters: ->
defered = @q.defer() 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.taskStatusById = groupBy(project.task_statuses, (x) -> x.id)
@scope.severityById = groupBy(project.severities, (x) -> x.id) @scope.severityById = groupBy(project.severities, (x) -> x.id)
@scope.priorityById = groupBy(project.priorities, (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) @scope.usStatusById = groupBy(project.us_statuses, (x) -> x.id)
return project return project
loadSearchData: (term) -> loadSearchData: (term = "") ->
promise = @rs.search.do(@scope.projectId, term).then (data) => @scope.loading = true
@scope.searchResults = data
return data
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: -> loadInitialData: ->
return @.loadProject().then (project) => return @.loadProject().then (project) =>
@scope.projectId = project.id @scope.projectId = project.id
@.fillUsersAndRoles(project.users, project.roles) @.fillUsersAndRoles(project.members, project.roles)
module.controller("SearchController", SearchController) module.controller("SearchController", SearchController)
@ -162,13 +176,22 @@ module.directive("tgSearchBox", SearchBoxDirective)
SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) ->
linkTable = ($scope, $el, $attrs, $ctrl) -> linkTable = ($scope, $el, $attrs, $ctrl) ->
applyAutoTab = true
activeSectionName = "userstories"
tabsDom = $el.find("section.search-filter") tabsDom = $el.find("section.search-filter")
lastSeatchResults = null lastSearchResults = null
getActiveSection = (data) -> getActiveSection = (data) ->
maxVal = 0 maxVal = 0
selectedSectionName = null selectedSection = {}
selectedSectionData = null selectedSection.name = "userstories"
selectedSection.value = []
if !applyAutoTab
selectedSection.name = activeSectionName
selectedSection.value = data[activeSectionName]
return selectedSection
if data if data
for name in ["userstories", "issues", "tasks", "wikipages"] for name in ["userstories", "issues", "tasks", "wikipages"]
@ -176,14 +199,14 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) ->
if value.length > maxVal if value.length > maxVal
maxVal = value.length maxVal = value.length
selectedSectionName = name selectedSection.name = name
selectedSectionData = value selectedSection.value = value
break; break;
if maxVal == 0 if maxVal == 0
return {name: "userstories", value: []} return selectedSection
return {name:selectedSectionName, value: selectedSectionData} return selectedSection
renderFilterTabs = (data) -> renderFilterTabs = (data) ->
for name, value of data for name, value of data
@ -195,6 +218,9 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) ->
tabsDom.find("a.active").removeClass("active") tabsDom.find("a.active").removeClass("active")
tabsDom.find("li.#{section.name} a").addClass("active") tabsDom.find("li.#{section.name} a").addClass("active")
applyAutoTab = false
activeSectionName = section.name
templates = { templates = {
issues: $templatecache.get("search-issues") issues: $templatecache.get("search-issues")
tasks: $templatecache.get("search-tasks") tasks: $templatecache.get("search-tasks")
@ -218,21 +244,26 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) ->
$el.find(".search-result-table").html(element) $el.find(".search-result-table").html(element)
$scope.$watch "searchResults", (data) -> $scope.$watch "searchResults", (data) ->
lastSeatchResults = data lastSearchResults = data
return if !lastSearchResults
activeSection = getActiveSection(data) activeSection = getActiveSection(data)
renderFilterTabs(data) renderFilterTabs(data)
renderTableContent(activeSection) renderTableContent(activeSection)
markSectionTabActive(activeSection) markSectionTabActive(activeSection)
$scope.$watch "searchTerm", (searchTerm) -> $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) -> $el.on "click", ".search-filter li > a", (event) ->
event.preventDefault() event.preventDefault()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
sectionName = target.parent().data("name") sectionName = target.parent().data("name")
sectionData = lastSeatchResults[sectionName] sectionData = if !lastSearchResults then [] else lastSearchResults[sectionName]
section = { section = {
name: sectionName, name: sectionName,

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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) @scope.$emit('project:loaded', project)
@.fillUsersAndRoles(project.users, project.roles) @.fillUsersAndRoles(project.members, project.roles)
return project 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]? if @scope.usTasks[task.user_story]? and @scope.usTasks[task.user_story][task.status]?
@scope.usTasks[task.user_story][task.status].push(task) @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 return tasks
loadTaskboard: -> loadTaskboard: ->

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
### ###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be> # Copyright (C) 2014-2015 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com> # Copyright (C) 2014-2015 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com> # Copyright (C) 2014-2015 David Barragán Merino <bameda@dbarragan.com>
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -23,6 +23,7 @@ taiga = @.taiga
mixOf = @.taiga.mixOf mixOf = @.taiga.mixOf
groupBy = @.taiga.groupBy groupBy = @.taiga.groupBy
bindMethods = @.taiga.bindMethods
module = angular.module("taigaTasks") 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, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
@log, @appMetaService, @navUrls, @analytics, @translate) -> @log, @appMetaService, @navUrls, @analytics, @translate) ->
bindMethods(@)
@scope.taskRef = @params.taskref @scope.taskRef = @params.taskref
@scope.sectionName = @translate.instant("TASK.SECTION_NAME") @scope.sectionName = @translate.instant("TASK.SECTION_NAME")
@.initializeEventHandlers() @.initializeEventHandlers()
@ -77,13 +80,10 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
initializeEventHandlers: -> initializeEventHandlers: ->
@scope.$on "attachment:create", => @scope.$on "attachment:create", =>
@analytics.trackEvent("attachment", "create", "create attachment on task", 1) @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", => @scope.$on "custom-attributes-values:edit", =>
@rootscope.$broadcast("object:updated") @rootscope.$broadcast("object:updated")
@scope.$on "comment:new", =>
@.loadTask()
initializeOnDeleteGoToUrl: -> initializeOnDeleteGoToUrl: ->
ctx = {project: @scope.project.slug} ctx = {project: @scope.project.slug}
@ -107,7 +107,6 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
@scope.$emit('project:loaded', project) @scope.$emit('project:loaded', project)
@scope.statusList = project.task_statuses @scope.statusList = project.task_statuses
@scope.statusById = groupBy(project.task_statuses, (x) -> x.id) @scope.statusById = groupBy(project.task_statuses, (x) -> x.id)
@scope.membersById = groupBy(project.memberships, (x) -> x.user)
return project return project
loadTask: -> loadTask: ->
@ -146,9 +145,53 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
loadInitialData: -> loadInitialData: ->
promise = @.loadProject() promise = @.loadProject()
return promise.then (project) => return promise.then (project) =>
@.fillUsersAndRoles(project.users, project.roles) @.fillUsersAndRoles(project.members, project.roles)
@.loadTask().then(=> @q.all([@.loadSprint(), @.loadUserStory()])) @.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) module.controller("TaskDetailController", TaskDetailController)
@ -199,7 +242,7 @@ module.directive("tgTaskStatusDisplay", ["$tgTemplate", "$compile", TaskStatusDi
## Task status button directive ## 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. # Display the status of Task and you can edit it.
# #
# Example: # Example:
@ -210,21 +253,7 @@ TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $co
# - scope.statusById object # - scope.statusById object
# - $scope.project.my_permissions # - $scope.project.my_permissions
template = _.template(""" template = $template.get("us/us-status-button.html", true)
<div class="status-data <% if(editable){ %>clickable<% }%>">
<span class="level" style="background-color:<%- status.color %>"></span>
<span class="status-status"><%- status.name %></span>
<% if(editable){ %><span class="icon icon-arrow-bottom"></span><% }%>
<span class="level-name" translate="COMMON.FIELDS.STATUS"></span>
<ul class="popover pop-status">
<% _.each(statuses, function(st) { %>
<li><a href="" class="status" title="<%- st.name %>"
data-status-id="<%- st.id %>"><%- st.name %></a></li>
<% }); %>
</ul>
</div>
""")
link = ($scope, $el, $attrs, $model) -> link = ($scope, $el, $attrs, $model) ->
isEditable = -> isEditable = ->
@ -292,7 +321,7 @@ TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $co
} }
module.directive("tgTaskStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQqueue", module.directive("tgTaskStatusButton", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQqueue",
"$compile", "$translate", TaskStatusButtonDirective]) "$compile", "$translate", "$tgTemplate", TaskStatusButtonDirective])
TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading, $qqueue, $compile) -> TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading, $qqueue, $compile) ->

Some files were not shown because too many files have changed in this diff Show More