diff --git a/.gitignore b/.gitignore index 71b48a51..18a1d2d1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ bower_components app/coffee/modules/locales/locale*.coffee *.swp *.swo +.#* tags tmp/ app/config/main.coffee diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..18a06b23 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +before_install: + - export CHROME_BIN=chromium-browser + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - npm install -g bower + - npm install -g gulp +install: + - npm install + - bower install +before_script: + - gulp deploy +language: node_js +node_js: + - "0.12" \ No newline at end of file diff --git a/AUTHORS.rst b/AUTHORS.rst index c1066e92..8f30a52f 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -26,3 +26,4 @@ answer newbie questions, and generally made Taiga that much better: - Daniel Koch - Florian Bezagu - Ryan Swanstrom +- Chris Wilson diff --git a/CHANGELOG.md b/CHANGELOG.md index d6c5e017..0ef083cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,45 @@ # Changelog # -## 1.7.0 Empetrum Nigrum (unreleased) +## 1.8.0 Saracenia Purpurea (2015-06-18) + +### Features +- Menus + - New User menu + - New project menu design +- Home + - Change home page for logged users, show a user dashboard with `working on` and `watching` sections. +- Proyects privacity + - Enabled public projects + - Improve SEO, fix meta tags and added social meta tags +- About project detail + - New projects list design + - New project detail page design + - Add project timeline +- User profile + - Now, access to edit user settings is out of a project + - New User profile view + - Add activity timeline to user profiles + - With the activity of my contacts on mine + - With the activity of the user on others + - Add user contacts to user profile + - Add project list to user profile +- Backlog panel + - Improve the drag & drop behavior of USs in backlog panel + - Select multiple US with `shift` in the backlog panel +- Global searches: + - Show the reference of entities in search results (thanks to [@artlepool](https://github.com/artlepool)) + - Autofocus on search modal +- i18n. + - Add deutsch (de) translation. + - Add nederlands (nl) translation. + +### Misc +- Improve performance: remove some unnecessary calls to the api. +- Lots of small and not so small bugfixes. + + +## 1.7.0 Empetrum Nigrum (2015-05-21) ### Features - Make Taiga translatable (i18n support). diff --git a/README.md b/README.md index 47039de2..dafbec35 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ![Kaleidos Project](http://kaleidos.net/static/img/badge.png "Kaleidos Project") [![Managed with Taiga](https://taiga.io/media/support/attachments/article-22/banner-gh.png)](https://taiga.io "Managed with Taiga") +[![Build Status](https://travis-ci.org/taigaio/taiga-front.svg?branch=public-header-bar)](https://travis-ci.org/taigaio/taiga-front) ## Get the compiled version ## diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index 31d0a01c..b6165763 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -28,129 +28,348 @@ taiga.generateHash = (components=[]) -> components = _.map(components, (x) -> JSON.stringify(x)) return hex_sha1(components.join(":")) + taiga.generateUniqueSessionIdentifier = -> date = (new Date()).getTime() randomNumber = Math.floor(Math.random() * 0x9000000) return taiga.generateHash([date, randomNumber]) + taiga.sessionId = taiga.generateUniqueSessionIdentifier() -configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEventsProvider, tgLoaderProvider, +configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEventsProvider, $compileProvider, $translateProvider) -> + + # wait until the trasnlation is ready to resolve the page + originalWhen = $routeProvider.when + + $routeProvider.when = (path, route) -> + route.resolve || (route.resolve = {}) + angular.extend(route.resolve, { + languageLoad: ["$q", "$translate", ($q, $translate) -> + deferred = $q.defer() + + $translate().then () -> deferred.resolve() + + return deferred.promise + ] + }) + + return originalWhen.call($routeProvider, path, route) + $routeProvider.when("/", - {templateUrl: "project/projects.html"}) + { + templateUrl: "home/home.html", + access: { + requiresLogin: true + }, + title: "HOME.PAGE_TITLE", + description: "HOME.PAGE_DESCRIPTION", + loader: true + } + ) + + $routeProvider.when("/projects/", + { + templateUrl: "projects/listing/projects-listing.html", + access: { + requiresLogin: true + }, + title: "PROJECTS.PAGE_TITLE", + description: "PROJECTS.PAGE_DESCRIPTION", + loader: true, + controller: "ProjectsListing", + controllerAs: "vm" + } + ) $routeProvider.when("/project/:pslug/", - {templateUrl: "project/project.html"}) + { + templateUrl: "projects/project/project.html", + loader: true, + controller: "Project", + controllerAs: "vm" + section: "project-timeline" + } + ) $routeProvider.when("/project/:pslug/search", - {templateUrl: "search/search.html", reloadOnSearch: false}) + { + templateUrl: "search/search.html", + reloadOnSearch: false, + section: "search" + } + ) $routeProvider.when("/project/:pslug/backlog", - {templateUrl: "backlog/backlog.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "backlog/backlog.html", + loader: true, + section: "backlog" + } + ) $routeProvider.when("/project/:pslug/kanban", - {templateUrl: "kanban/kanban.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "kanban/kanban.html", + loader: true, + section: "kanban" + } + ) # Milestone $routeProvider.when("/project/:pslug/taskboard/:sslug", - {templateUrl: "taskboard/taskboard.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "taskboard/taskboard.html", + loader: true, + section: "backlog" + } + ) # User stories $routeProvider.when("/project/:pslug/us/:usref", - {templateUrl: "us/us-detail.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "us/us-detail.html", + loader: true, + section: "backlog-kanban" + } + ) # Tasks $routeProvider.when("/project/:pslug/task/:taskref", - {templateUrl: "task/task-detail.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "task/task-detail.html", + loader: true, + section: "backlog-kanban" + } + ) # Wiki $routeProvider.when("/project/:pslug/wiki", {redirectTo: (params) -> "/project/#{params.pslug}/wiki/home"}, ) $routeProvider.when("/project/:pslug/wiki/:slug", - {templateUrl: "wiki/wiki.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "wiki/wiki.html", + loader: true, + section: "wiki" + } + ) # Team $routeProvider.when("/project/:pslug/team", - {templateUrl: "team/team.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "team/team.html", + loader: true, + section: "team" + } + ) # Issues $routeProvider.when("/project/:pslug/issues", - {templateUrl: "issue/issues.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "issue/issues.html", + loader: true, + section: "issues" + } + ) $routeProvider.when("/project/:pslug/issue/:issueref", - {templateUrl: "issue/issues-detail.html", resolve: {loader: tgLoaderProvider.add()}}) + { + templateUrl: "issue/issues-detail.html", + loader: true, + section: "issues" + } + ) # Admin - Project Profile $routeProvider.when("/project/:pslug/admin/project-profile/details", - {templateUrl: "admin/admin-project-profile.html"}) + { + templateUrl: "admin/admin-project-profile.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-profile/default-values", - {templateUrl: "admin/admin-project-default-values.html"}) + { + templateUrl: "admin/admin-project-default-values.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-profile/modules", - {templateUrl: "admin/admin-project-modules.html"}) + { + templateUrl: "admin/admin-project-modules.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-profile/export", - {templateUrl: "admin/admin-project-export.html"}) + { + templateUrl: "admin/admin-project-export.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-profile/reports", - {templateUrl: "admin/admin-project-reports.html"}) + { + templateUrl: "admin/admin-project-reports.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-values/status", - {templateUrl: "admin/admin-project-values-status.html"}) + { + templateUrl: "admin/admin-project-values-status.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-values/points", - {templateUrl: "admin/admin-project-values-points.html"}) + { + templateUrl: "admin/admin-project-values-points.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-values/priorities", - {templateUrl: "admin/admin-project-values-priorities.html"}) + { + templateUrl: "admin/admin-project-values-priorities.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-values/severities", - {templateUrl: "admin/admin-project-values-severities.html"}) + { + templateUrl: "admin/admin-project-values-severities.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-values/types", - {templateUrl: "admin/admin-project-values-types.html"}) + { + templateUrl: "admin/admin-project-values-types.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/project-values/custom-fields", - {templateUrl: "admin/admin-project-values-custom-fields.html"}) + { + templateUrl: "admin/admin-project-values-custom-fields.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/memberships", - {templateUrl: "admin/admin-memberships.html"}) + { + templateUrl: "admin/admin-memberships.html", + section: "admin" + } + ) # Admin - Roles $routeProvider.when("/project/:pslug/admin/roles", - {templateUrl: "admin/admin-roles.html"}) + { + templateUrl: "admin/admin-roles.html", + section: "admin" + } + ) + # Admin - Third Parties $routeProvider.when("/project/:pslug/admin/third-parties/webhooks", - {templateUrl: "admin/admin-third-parties-webhooks.html"}) + { + templateUrl: "admin/admin-third-parties-webhooks.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/third-parties/github", - {templateUrl: "admin/admin-third-parties-github.html"}) + { + templateUrl: "admin/admin-third-parties-github.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/third-parties/gitlab", - {templateUrl: "admin/admin-third-parties-gitlab.html"}) + { + templateUrl: "admin/admin-third-parties-gitlab.html", + section: "admin" + } + ) $routeProvider.when("/project/:pslug/admin/third-parties/bitbucket", - {templateUrl: "admin/admin-third-parties-bitbucket.html"}) + { + templateUrl: "admin/admin-third-parties-bitbucket.html", + section: "admin" + } + ) # Admin - Contrib Plugins $routeProvider.when("/project/:pslug/admin/contrib/:plugin", {templateUrl: "contrib/main.html"}) # User settings - $routeProvider.when("/project/:pslug/user-settings/user-profile", + $routeProvider.when("/user-settings/user-profile", {templateUrl: "user/user-profile.html"}) - $routeProvider.when("/project/:pslug/user-settings/user-change-password", + $routeProvider.when("/user-settings/user-change-password", {templateUrl: "user/user-change-password.html"}) - $routeProvider.when("/project/:pslug/user-settings/user-avatar", - {templateUrl: "user/user-avatar.html"}) - $routeProvider.when("/project/:pslug/user-settings/mail-notifications", + $routeProvider.when("/user-settings/mail-notifications", {templateUrl: "user/mail-notifications.html"}) $routeProvider.when("/change-email/:email_token", {templateUrl: "user/change-email.html"}) $routeProvider.when("/cancel-account/:cancel_token", {templateUrl: "user/cancel-account.html"}) + # User profile + $routeProvider.when("/profile", + { + templateUrl: "profile/profile.html", + loader: true, + access: { + requiresLogin: true + }, + controller: "Profile", + controllerAs: "vm" + } + ) + + $routeProvider.when("/profile/:slug", + { + templateUrl: "profile/profile.html", + loader: true, + controller: "Profile", + controllerAs: "vm" + } + ) + # Auth $routeProvider.when("/login", - {templateUrl: "auth/login.html"}) + { + templateUrl: "auth/login.html", + title: "LOGIN.PAGE_TITLE" + description: "LOGIN.PAGE_DESCRIPTION" + } + ) $routeProvider.when("/register", - {templateUrl: "auth/register.html"}) + { + templateUrl: "auth/register.html", + title: "REGISTER.PAGE_TITLE", + description: "REGISTER.PAGE_DESCRIPTION" + } + ) $routeProvider.when("/forgot-password", - {templateUrl: "auth/forgot-password.html"}) + { + templateUrl: "auth/forgot-password.html", + title: "FORGOT_PASSWORD.PAGE_TITLE", + description: "FORGOT_PASSWORD.PAGE_DESCRIPTION" + } + ) $routeProvider.when("/change-password", - {templateUrl: "auth/change-password-from-recovery.html"}) + { + templateUrl: "auth/change-password-from-recovery.html", + title: "CHANGE_PASSWORD.PAGE_TITLE", + description: "CHANGE_PASSWORD.PAGE_TITLE", + } + ) $routeProvider.when("/change-password/:token", - {templateUrl: "auth/change-password-from-recovery.html"}) + { + templateUrl: "auth/change-password-from-recovery.html", + title: "CHANGE_PASSWORD.PAGE_TITLE", + description: "CHANGE_PASSWORD.PAGE_TITLE", + } + ) $routeProvider.when("/invitation/:token", - {templateUrl: "auth/invitation.html"}) + { + templateUrl: "auth/invitation.html", + title: "INVITATION.PAGE_TITLE", + description: "INVITATION.PAGE_DESCRIPTION" + } + ) # Errors/Exceptions $routeProvider.when("/error", @@ -177,6 +396,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven "X-Session-Id": taiga.sessionId } + $httpProvider.useApplyAsync(true) + $tgEventsProvider.setSessionId(taiga.sessionId) # Add next param when user try to access to a secction need auth permissions. @@ -201,6 +422,35 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven $httpProvider.interceptors.push("authHttpIntercept") + + loaderIntercept = ($q, loaderService) -> + return { + request: (config) -> + loaderService.logRequest() + + return config + + requestError: (rejection) -> + loaderService.logResponse() + + return $q.reject(rejection) + + responseError: (rejection) -> + loaderService.logResponse() + + return $q.reject(rejection) + + response: (response) -> + loaderService.logResponse() + + return response + } + + + $provide.factory("loaderIntercept", ["$q", "tgLoader", loaderIntercept]) + + $httpProvider.interceptors.push("loaderIntercept") + # If there is an error in the version throw a notify error. # IMPROVEiMENT: Move this version error handler to USs, issues and tasks repository versionCheckHttpIntercept = ($q) -> @@ -286,7 +536,7 @@ i18nInit = (lang, $translate) -> checksley.updateMessages('default', messages) -init = ($log, $config, $rootscope, $auth, $events, $analytics, $translate) -> +init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, projectService, loaderService) -> $log.debug("Initialize application") # Taiga Plugins @@ -297,6 +547,10 @@ init = ($log, $config, $rootscope, $auth, $events, $analytics, $translate) -> lang = ctx.language i18nInit(lang, $translate) + # bluebird + Promise.setScheduler (cb) -> + $rootscope.$evalAsync(cb) + # Load user if $auth.isAuthenticated() $events.setupConnection() @@ -305,16 +559,50 @@ init = ($log, $config, $rootscope, $auth, $events, $analytics, $translate) -> # Analytics $analytics.initialize() + # On the first page load the loader is painted in `$routeChangeSuccess` + # because we need to hide the tg-navigation-bar. + # In the other cases the loader is in `$routeChangeSuccess` + # because `location.noreload` prevent to execute this event. + un = $rootscope.$on '$routeChangeStart', (event, next) -> + if next.loader + loaderService.start(true) + + un() + + $rootscope.$on '$routeChangeSuccess', (event, next) -> + if next.loader + loaderService.start(true) + + if next.access && next.access.requiresLogin + if !$auth.isAuthenticated() + $location.path($navUrls.resolve("login")) + + projectService.setSection(next.section) + + if next.params.pslug + projectService.setProject(next.params.pslug) + else + projectService.cleanProject() + + if next.title or next.description + title = $translate.instant(next.title or "") + description = $translate.instant(next.description or "") + appMetaService.setAll(title, description) + modules = [ # Main Global Modules "taigaBase", "taigaCommon", "taigaResources", + "taigaResources2", "taigaAuth", "taigaEvents", # Specific Modules + "taigaHome", + "taigaNavigationBar", + "taigaProjects", "taigaRelatedTasks", "taigaBacklog", "taigaTaskboard", @@ -326,13 +614,16 @@ modules = [ "taigaWiki", "taigaSearch", "taigaAdmin", - "taigaNavMenu", "taigaProject", "taigaUserSettings", "taigaFeedback", "taigaPlugins", "taigaIntegrations", "taigaComponents", + # new modules + "taigaProfile", + "taigaHome", + "taigaUserTimeline", # template cache "templates", @@ -340,7 +631,9 @@ modules = [ # Vendor modules "ngRoute", "ngAnimate", - "pascalprecht.translate" + "pascalprecht.translate", + "infinite-scroll", + "tgRepeat" ].concat(_.map(@.taigaContribPlugins, (plugin) -> plugin.module)) # Main module definition @@ -352,7 +645,6 @@ module.config([ "$httpProvider", "$provide", "$tgEventsProvider", - "tgLoaderProvider", "$compileProvider", "$translateProvider", configure @@ -360,11 +652,15 @@ module.config([ module.run([ "$log", - "$tgConfig", "$rootScope", "$tgAuth", "$tgEvents", "$tgAnalytics", - "$translate" + "$translate", + "$tgLocation", + "$tgNavUrls", + "tgAppMetaService", + "tgProjectService", + "tgLoader", init ]) diff --git a/app/coffee/modules/admin/memberships.coffee b/app/coffee/modules/admin/memberships.coffee index d5b6deec..9d46467c 100644 --- a/app/coffee/modules/admin/memberships.coffee +++ b/app/coffee/modules/admin/memberships.coffee @@ -43,11 +43,12 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai "$tgLocation", "$tgNavUrls", "$tgAnalytics", - "$appTitle" + "tgAppMetaService", + "$translate" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, - @location, @navUrls, @analytics, @appTitle) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @analytics, + @appMetaService, @translate) -> bindMethods(@) @scope.project = {} @@ -56,7 +57,9 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai promise = @.loadInitialData() promise.then => - @appTitle.set("Membership - " + @scope.project.name) + title = @translate.instant("ADMIN.MEMBERSHIPS.PAGE_TITLE", {projectName: @scope.project.name}) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) @@ -65,10 +68,11 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai @analytics.trackEvent("membership", "create", "create memberships on admin", 1) loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_owner @location.path(@navUrls.resolve("permission-denied")) + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) return project @@ -76,20 +80,20 @@ class MembershipsController extends mixOf(taiga.Controller, taiga.PageMixin, tai loadMembers: -> httpFilters = @.getUrlFilters() return @rs.memberships.list(@scope.projectId, httpFilters).then (data) => - @scope.memberships = _.filter(data.models, (membership) -> membership.user == null or membership.is_user_active) + @scope.memberships = _.filter(data.models, (membership) -> + membership.user == null or membership.is_user_active) @scope.page = data.current @scope.count = data.count @scope.paginatedBy = data.paginatedBy return data loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data + promise = @.loadProject() + promise.then => + @.loadUsersAndRoles() + @.loadMembers() - return promise.then(=> @.loadProject()) - .then(=> @.loadUsersAndRoles()) - .then(=> @.loadMembers()) + return promise getUrlFilters: -> filters = _.pick(@location.search(), "page") @@ -377,7 +381,9 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla $el.on "click", ".pending", (event) -> event.preventDefault() onSuccess = -> - text = $translate.instant("ADMIN.MEMBERSHIP.SUCCESS_SEND_INVITATION", {email: $scope.member.email}) + text = $translate.instant("ADMIN.MEMBERSHIP.SUCCESS_SEND_INVITATION", { + email: $scope.member.email + }) $confirm.notify("success", text) onError = -> text = $translate.instant("ADMIM.MEMBERSHIP.ERROR_SEND_INVITATION") @@ -414,4 +420,5 @@ MembershipsRowActionsDirective = ($log, $repo, $rs, $confirm, $compile, $transla return {link: link} -module.directive("tgMembershipsRowActions", ["$log", "$tgRepo", "$tgResources", "$tgConfirm", "$compile", "$translate", MembershipsRowActionsDirective]) +module.directive("tgMembershipsRowActions", ["$log", "$tgRepo", "$tgResources", "$tgConfirm", "$compile", + "$translate", MembershipsRowActionsDirective]) diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee index 207734b4..6cf7d5a9 100644 --- a/app/coffee/modules/admin/project-profile.coffee +++ b/app/coffee/modules/admin/project-profile.coffee @@ -47,34 +47,38 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) "$q", "$tgLocation", "$tgNavUrls", - "$appTitle", + "tgAppMetaService", "$translate" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @appTitle, @translate) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, + @appMetaService, @translate) -> @scope.project = {} promise = @.loadInitialData() promise.then => sectionName = @translate.instant( @scope.sectionName) - appTitle = @translate.instant("ADMIN.PROJECT_PROFILE.PAGE_TITLE", { + title = @translate.instant("ADMIN.PROJECT_PROFILE.PAGE_TITLE", { sectionName: sectionName, projectName: @scope.project.name}) - @appTitle.set(appTitle) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) @scope.$on "project:loaded", => sectionName = @translate.instant(@scope.sectionName) - appTitle = @translate.instant("ADMIN.PROJECT_PROFILE.PAGE_TITLE", { + title = @translate.instant("ADMIN.PROJECT_PROFILE.PAGE_TITLE", { sectionName: sectionName, projectName: @scope.project.name}) - @appTitle.set(appTitle) + description = @scope.project.description + @appMetaService.setAll(title, description) loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_owner @location.path(@navUrls.resolve("permission-denied")) + @scope.projectId = project.id @scope.project = project @scope.pointsList = _.sortBy(project.points, "order") @scope.usStatusList = _.sortBy(project.us_statuses, "order") @@ -86,22 +90,9 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$emit('project:loaded', project) return project - loadTagsColors: -> - return @rs.projects.tagsColors(@scope.projectId).then (tags_colors) => - @scope.project.tags_colors = tags_colors - - loadProjectProfile: -> - return @q.all([ - @.loadProject(), - @.loadTagsColors() - ]) - loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProjectProfile()) + promise = @.loadProject() + return promise openDeleteLightbox: -> @rootscope.$broadcast("deletelightbox:new", @scope.project) @@ -113,8 +104,10 @@ module.controller("ProjectProfileController", ProjectProfileController) ## Project Profile Directive ############################################################################# -ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) -> +ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, projectService) -> link = ($scope, $el, $attrs) -> + $ctrl = $el.controller() + form = $el.find("form").checksley({"onlyOneErrorElement": true}) submit = debounce 2000, (event) => event.preventDefault() @@ -127,9 +120,14 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) -> promise.then -> $loading.finish(submitButton) $confirm.notify("success") - newUrl = $navurls.resolve("project-admin-project-profile-details", {project: $scope.project.slug}) + newUrl = $navurls.resolve("project-admin-project-profile-details", { + project: $scope.project.slug + }) $location.path(newUrl) - $scope.$emit("project:loaded", $scope.project) + + $ctrl.loadInitialData() + + projectService.fetchProject() promise.then null, (data) -> $loading.finish(submitButton) @@ -144,7 +142,8 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location) -> return {link:link} module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", "$tgNavUrls", "$tgLocation", - ProjectProfileDirective]) + "tgProjectService", ProjectProfileDirective]) + ############################################################################# ## Project Default Values Directive @@ -187,7 +186,7 @@ module.directive("tgProjectDefaultValues", ["$tgRepo", "$tgConfirm", "$tgLoading ## Project Modules Directive ############################################################################# -ProjectModulesDirective = ($repo, $confirm, $loading) -> +ProjectModulesDirective = ($repo, $confirm, $loading, projectService) -> link = ($scope, $el, $attrs) -> form = $el.find("form").checksley() submit = => @@ -201,6 +200,8 @@ ProjectModulesDirective = ($repo, $confirm, $loading) -> $confirm.notify("success") $scope.$emit("project:loaded", $scope.project) + projectService.fetchProject() + promise.then null, (data) -> $loading.finish(target) $confirm.notify("error", data._error_message) @@ -229,7 +230,8 @@ ProjectModulesDirective = ($repo, $confirm, $loading) -> return {link:link} -module.directive("tgProjectModules", ["$tgRepo", "$tgConfirm", "$tgLoading", ProjectModulesDirective]) +module.directive("tgProjectModules", ["$tgRepo", "$tgConfirm", "$tgLoading", "tgProjectService", + ProjectModulesDirective]) ############################################################################# diff --git a/app/coffee/modules/admin/project-values.coffee b/app/coffee/modules/admin/project-values.coffee index c86c762d..2a58f101 100644 --- a/app/coffee/modules/admin/project-values.coffee +++ b/app/coffee/modules/admin/project-values.coffee @@ -46,11 +46,12 @@ class ProjectValuesSectionController extends mixOf(taiga.Controller, taiga.PageM "$q", "$tgLocation", "$tgNavUrls", - "$appTitle", + "tgAppMetaService", "$translate" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @appTitle, @translate) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, + @appMetaService, @translate) -> @scope.project = {} promise = @.loadInitialData() @@ -58,30 +59,28 @@ class ProjectValuesSectionController extends mixOf(taiga.Controller, taiga.PageM promise.then () => sectionName = @translate.instant(@scope.sectionName) - title = @translate.instant("ADMIN.PROJECT_VALUES.APP_TITLE", { + title = @translate.instant("ADMIN.PROJECT_VALUES.PAGE_TITLE", { "sectionName": sectionName, "projectName": @scope.project.name }) - - @appTitle.set(title) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_owner @location.path(@navUrls.resolve("permission-denied")) + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) return project loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then => @.loadProject() + promise = @.loadProject() + return promise module.controller("ProjectValuesSectionController", ProjectValuesSectionController) @@ -126,7 +125,7 @@ module.controller("ProjectValuesController", ProjectValuesController) ## Project values directive ############################################################################# -ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, @translate, $rootscope) -> +ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, $translate, $rootscope) -> ## Drag & Drop Link linkDragAndDrop = ($scope, $el, $attrs) -> @@ -167,7 +166,7 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, @tra } initializeTextTranslations = -> - $scope.addNewElementText = @translate.instant("ADMIN.PROJECT_VALUES_#{objName.toUpperCase()}.ACTION_ADD") + $scope.addNewElementText = $translate.instant("ADMIN.PROJECT_VALUES_#{objName.toUpperCase()}.ACTION_ADD") initializeNewValue() initializeTextTranslations() @@ -296,7 +295,10 @@ ProjectValuesDirective = ($log, $repo, $confirm, $location, animationFrame, @tra if _.keys(choices).length == 0 return $confirm.error("ADMIN.PROJECT_VALUES.ERROR_DELETE_ALL") - $confirm.askChoice("PROJECT.TITLE_ACTION_DELETE_VALUE", subtitle, choices, "ADMIN.PROJECT_VALUES.REPLACEMENT").then (response) -> + title = $translate.instant("ADMIN.COMMON.TITLE_ACTION_DELETE_VALUE") + text = $translate.instant("ADMIN.PROJECT_VALUES.REPLACEMENT") + + $confirm.askChoice(title, subtitle, choices, text).then (response) -> onSucces = -> $ctrl.loadValues().finally -> response.finish() @@ -382,15 +384,24 @@ class ProjectCustomAttributesController extends mixOf(taiga.Controller, taiga.Pa "$q", "$tgLocation", "$tgNavUrls", - "$appTitle", + "tgAppMetaService", + "$translate" ] - constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appTitle) -> + constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appMetaService, + @translate) -> @scope.project = {} @rootscope.$on "project:loaded", => @.loadCustomAttributes() - @appTitle.set("Project Custom Attributes - " + @scope.sectionName + " - " + @scope.project.name) + + sectionName = @translate.instant(@scope.sectionName) + title = @translate.instant("ADMIN.CUSTOM_ATTRIBUTES.PAGE_TITLE", { + "sectionName": sectionName, + "projectName": @scope.project.name + }) + description = @scope.project.description + @appMetaService.setAll(title, description) ######################### # Custom Attribute @@ -430,7 +441,7 @@ module.controller("ProjectCustomAttributesController", ProjectCustomAttributesCo ## Custom Attributes Directive ############################################################################# -ProjectCustomAttributesDirective = ($log, $confirm, animationFrame) -> +ProjectCustomAttributesDirective = ($log, $confirm, animationFrame, $translate) -> link = ($scope, $el, $attrs) -> $ctrl = $el.controller() @@ -616,7 +627,10 @@ ProjectCustomAttributesDirective = ($log, $confirm, animationFrame) -> attr = formEl.scope().attr message = attr.name - $confirm.ask("COMMON.CUSTOM_ATTRIBUTES.DELETE", "COMMON.CUSTOM_ATTRIBUTES.CONFIRM_DELETE", message).then (finish) -> + title = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.DELETE") + text = $translate.instant("COMMON.CUSTOM_ATTRIBUTES.CONFIRM_DELETE") + + $confirm.ask(title, text, message).then (finish) -> onSucces = -> $ctrl.loadCustomAttributes().finally -> finish() @@ -636,4 +650,5 @@ ProjectCustomAttributesDirective = ($log, $confirm, animationFrame) -> return {link: link} -module.directive("tgProjectCustomAttributes", ["$log", "$tgConfirm", "animationFrame", ProjectCustomAttributesDirective]) +module.directive("tgProjectCustomAttributes", ["$log", "$tgConfirm", "animationFrame", "$translate", + ProjectCustomAttributesDirective]) diff --git a/app/coffee/modules/admin/roles.coffee b/app/coffee/modules/admin/roles.coffee index 9e3b1669..c678d775 100644 --- a/app/coffee/modules/admin/roles.coffee +++ b/app/coffee/modules/admin/roles.coffee @@ -44,12 +44,12 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil "$q", "$tgLocation", "$tgNavUrls", - "$appTitle", + "tgAppMetaService", "$translate" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @appTitle, - @translate) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, + @appMetaService, @translate) -> bindMethods(@) @scope.sectionName = "ADMIN.MENU.PERMISSIONS" @@ -59,16 +59,18 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil promise = @.loadInitialData() promise.then () => - title = @translate.instant("ADMIN.ROLES.SECTION_NAME", {projectName: @scope.project.name}) - @appTitle.set(title) + title = @translate.instant("ADMIN.ROLES.PAGE_TITLE", {projectName: @scope.project.name}) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_owner @location.path(@navUrls.resolve("permission-denied")) + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) @@ -76,39 +78,29 @@ class RolesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fil return project - loadExternalUserRole: (roles) -> - roles = roles.map (role) -> - role.external_user = false - - return role - - public_permission = { - "name": @translate.instant("ADMIN.ROLES.EXTERNAL_USER"), - "permissions": @scope.project.public_permissions, - "external_user": true - } - - roles.push(public_permission) - - return roles - loadRoles: -> - return @rs.roles.list(@scope.projectId) - .then @loadExternalUserRole - .then (roles) => - @scope.roles = roles - @scope.role = @scope.roles[0] + return @rs.roles.list(@scope.projectId).then (roles) => + roles = roles.map (role) -> + role.external_user = false - return roles + return role + + public_permission = { + "name": @translate.instant("ADMIN.ROLES.EXTERNAL_USER"), + "permissions": @scope.project.public_permissions, + "external_user": true + } + + roles.push(public_permission) + + @scope.roles = roles + @scope.role = @scope.roles[0] + return roles loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProject()) - .then(=> @.loadUsersAndRoles()) - .then(=> @.loadRoles()) + promise = @.loadProject() + promise.then(=> @.loadRoles()) + return promise setRole: (role) -> @scope.role = role @@ -236,7 +228,8 @@ NewRoleDirective = ($tgrepo, $confirm) -> $el.find(".new").val('') onSuccess = (role) -> - $scope.roles.push(role) + insertPosition = $scope.roles.length - 1 + $scope.roles.splice(insertPosition, 0, role) $ctrl.setRole(role) $el.find(".add-button").show() $ctrl.loadProject() diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee index 06bb946e..737a8144 100644 --- a/app/coffee/modules/admin/third-parties.coffee +++ b/app/coffee/modules/admin/third-parties.coffee @@ -28,6 +28,7 @@ timeout = @.taiga.timeout module = angular.module("taigaAdmin") + ############################################################################# ## Webhooks ############################################################################# @@ -40,11 +41,11 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga. "$routeParams", "$tgLocation", "$tgNavUrls", - "$appTitle", + "tgAppMetaService", "$translate" ] - constructor: (@scope, @repo, @rs, @params, @location, @navUrls, @appTitle, @translate) -> + constructor: (@scope, @repo, @rs, @params, @location, @navUrls, @appMetaService, @translate) -> bindMethods(@) @scope.sectionName = "ADMIN.WEBHOOKS.SECTION_NAME" @@ -53,8 +54,9 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga. promise = @.loadInitialData() promise.then () => - text = @translate.instant("ADMIN.WEBHOOKS.APP_TITLE", {"projectName": @scope.project.name}) - @appTitle.set(text) + title = @translate.instant("ADMIN.WEBHOOKS.PAGE_TITLE", {projectName: @scope.project.name}) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) @@ -65,24 +67,25 @@ class WebhooksController extends mixOf(taiga.Controller, taiga.PageMixin, taiga. @scope.webhooks = webhooks loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => if not project.i_am_owner @location.path(@navUrls.resolve("permission-denied")) + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) return project loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data + promise = @.loadProject() + promise.then => + @.loadWebhooks() - return promise.then(=> @.loadProject()) - .then(=> @.loadWebhooks()) + return promise module.controller("WebhooksController", WebhooksController) + ############################################################################# ## Webhook Directive ############################################################################# @@ -213,7 +216,8 @@ WebhookDirective = ($rs, $repo, $confirm, $loading, $translate) -> return {link:link} -module.directive("tgWebhook", ["$tgResources", "$tgRepo", "$tgConfirm", "$tgLoading", "$translate", WebhookDirective]) +module.directive("tgWebhook", ["$tgResources", "$tgRepo", "$tgConfirm", "$tgLoading", "$translate", + WebhookDirective]) ############################################################################# @@ -289,11 +293,11 @@ class GithubController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi "$tgRepo", "$tgResources", "$routeParams", - "$appTitle", + "tgAppMetaService", "$translate" ] - constructor: (@scope, @repo, @rs, @params, @appTitle, @translate) -> + constructor: (@scope, @repo, @rs, @params, @appMetaService, @translate) -> bindMethods(@) @scope.sectionName = @translate.instant("ADMIN.GITHUB.SECTION_NAME") @@ -302,8 +306,9 @@ class GithubController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi promise = @.loadInitialData() promise.then () => - title = @translate.instant("ADMIN.GITHUB.APP_TITLE", {projectName: @scope.project.name}) - @appTitle.set(title) + title = @translate.instant("ADMIN.GITHUB.PAGE_TITLE", {projectName: @scope.project.name}) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) @@ -312,19 +317,16 @@ class GithubController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.github = github loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) return project loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProject()) - .then(=> @.loadModules()) - + promise = @.loadProject() + promise.then(=> @.loadModules()) + return promise module.controller("GithubController", GithubController) @@ -339,11 +341,11 @@ class GitlabController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi "$tgRepo", "$tgResources", "$routeParams", - "$appTitle", + "tgAppMetaService", "$translate" ] - constructor: (@scope, @repo, @rs, @params, @appTitle, @translate) -> + constructor: (@scope, @repo, @rs, @params, @appMetaService, @translate) -> bindMethods(@) @scope.sectionName = @translate.instant("ADMIN.GITLAB.SECTION_NAME") @@ -351,8 +353,9 @@ class GitlabController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi promise = @.loadInitialData() promise.then () => - title = @translate.instant("ADMIN.GITLAB.APP_TITLE", {projectName: @scope.project.name}) - @appTitle.set(title) + title = @translate.instant("ADMIN.GITLAB.PAGE_TITLE", {projectName: @scope.project.name}) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) @@ -364,19 +367,16 @@ class GitlabController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi @scope.gitlab = gitlab loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) return project loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProject()) - .then(=> @.loadModules()) - + promise = @.loadProject() + promise.then(=> @.loadModules()) + return promise module.controller("GitlabController", GitlabController) @@ -391,11 +391,11 @@ class BitbucketController extends mixOf(taiga.Controller, taiga.PageMixin, taiga "$tgRepo", "$tgResources", "$routeParams", - "$appTitle", + "tgAppMetaService", "$translate" ] - constructor: (@scope, @repo, @rs, @params, @appTitle, @translate) -> + constructor: (@scope, @repo, @rs, @params, @appMetaService, @translate) -> bindMethods(@) @scope.sectionName = @translate.instant("ADMIN.BITBUCKET.SECTION_NAME") @@ -403,8 +403,9 @@ class BitbucketController extends mixOf(taiga.Controller, taiga.PageMixin, taiga promise = @.loadInitialData() promise.then () => - title = @translate.instant("ADMIN.BITBUCKET.APP_TITLE", {projectName: @scope.project.name}) - @appTitle.set(title) + title = @translate.instant("ADMIN.BITBUCKET.PAGE_TITLE", {projectName: @scope.project.name}) + description = @scope.project.description + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) @@ -416,18 +417,16 @@ class BitbucketController extends mixOf(taiga.Controller, taiga.PageMixin, taiga @scope.bitbucket = bitbucket loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) return project loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProject()) - .then(=> @.loadModules()) + promise = @.loadProject() + promise.then(=> @.loadModules()) + return promise module.controller("BitbucketController", BitbucketController) @@ -552,12 +551,12 @@ module.directive("tgBitbucketWebhooks", ["$tgRepo", "$tgConfirm", "$tgLoading", ############################################################################# ValidOriginIpsDirective = -> link = ($scope, $el, $attrs, $ngModel) -> - $ngModel.$parsers.push (value) -> - value = $.trim(value) - if value == "" - return [] + $ngModel.$parsers.push (value) -> + value = $.trim(value) + if value == "" + return [] - return value.split(",") + return value.split(",") return { link: link diff --git a/app/coffee/modules/auth.coffee b/app/coffee/modules/auth.coffee index 7735eb04..17c13a4a 100644 --- a/app/coffee/modules/auth.coffee +++ b/app/coffee/modules/auth.coffee @@ -36,14 +36,26 @@ class AuthService extends taiga.Service "$tgHttp", "$tgUrls", "$tgConfig", - "$translate"] + "$translate", + "tgCurrentUserService"] - constructor: (@rootscope, @storage, @model, @rs, @http, @urls, @config, @translate) -> + constructor: (@rootscope, @storage, @model, @rs, @http, @urls, @config, @translate, @currentUserService) -> super() + userModel = @.getUser() + @.setUserdata(userModel) + + setUserdata: (userModel) -> + if userModel + @.userData = Immutable.fromJS(userModel.getAttrs()) + @currentUserService.setUser(@.userData) + else + @.userData = null + _setLocales: -> lang = @rootscope.user.lang || @config.get("defaultLanguage") || "en" - @translate.use(lang) + @translate.preferredLanguage(lang) # Needed for calls to the api in the correct language + @translate.use(lang) # Needed for change the interface in runtime getUser: -> if @rootscope.user @@ -63,6 +75,8 @@ class AuthService extends taiga.Service @storage.set("userInfo", user.getAttrs()) @rootscope.user = user + @.setUserdata(user) + @._setLocales() clear: -> @@ -104,6 +118,8 @@ class AuthService extends taiga.Service @.removeToken() @.clear() + @currentUserService.removeUser() + register: (data, type, existing) -> url = @urls.resolve("auth-register") @@ -176,7 +192,8 @@ PublicRegisterMessageDirective = ($config, $navUrls, templates) -> template: templateFn } -module.directive("tgPublicRegisterMessage", ["$tgConfig", "$tgNavUrls", "$tgTemplate", PublicRegisterMessageDirective]) +module.directive("tgPublicRegisterMessage", ["$tgConfig", "$tgNavUrls", "$tgTemplate", + PublicRegisterMessageDirective]) LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $events, $translate) -> @@ -212,11 +229,17 @@ LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $ $el.on "submit", "form", submit + window.prerenderReady = true + + $scope.$on "$destroy", -> + $el.off() + return {link:link} module.directive("tgLogin", ["$tgAuth", "$tgConfirm", "$tgLocation", "$tgConfig", "$routeParams", "$tgNavUrls", "$tgEvents", "$translate", LoginDirective]) + ############################################################################# ## Register Directive ############################################################################# @@ -238,9 +261,9 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics, $location.path($navUrls.resolve("home")) onErrorSubmit = (response) -> - if response.data._error_message? - text = $translate.instant("LOGIN_FORM.ERROR_GENERIC") + " " + response.data._error_message - $confirm.notify("light-error", text + " " + response.data._error_message) + if response.data._error_message + text = $translate.instant("COMMON.GENERIC_ERROR", {error: response.data._error_message}) + $confirm.notify("light-error", text) form.setErrors(response.data) @@ -255,11 +278,17 @@ RegisterDirective = ($auth, $confirm, $location, $navUrls, $config, $analytics, $el.on "submit", "form", submit + $scope.$on "$destroy", -> + $el.off() + + window.prerenderReady = true + return {link:link} module.directive("tgRegister", ["$tgAuth", "$tgConfirm", "$tgLocation", "$tgNavUrls", "$tgConfig", "$tgAnalytics", "$translate", RegisterDirective]) + ############################################################################# ## Forgot Password Directive ############################################################################# @@ -291,11 +320,17 @@ ForgotPasswordDirective = ($auth, $confirm, $location, $navUrls, $translate) -> $el.on "submit", "form", submit + $scope.$on "$destroy", -> + $el.off() + + window.prerenderReady = true + return {link:link} module.directive("tgForgotPassword", ["$tgAuth", "$tgConfirm", "$tgLocation", "$tgNavUrls", "$translate", ForgotPasswordDirective]) + ############################################################################# ## Change Password from Recovery Directive ############################################################################# @@ -316,12 +351,10 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav $location.path($navUrls.resolve("login")) text = $translate.instant("CHANGE_PASSWORD_RECOVERY_FORM.SUCCESS") - $confirm.success(text) onErrorSubmit = (response) -> text = $translate.instant("COMMON.GENERIC_ERROR", {error: response.data._error_message}) - $confirm.notify("light-error", text) submit = debounce 2000, (event) => @@ -335,10 +368,15 @@ ChangePasswordFromRecoveryDirective = ($auth, $confirm, $location, $params, $nav $el.on "submit", "form", submit + $scope.$on "$destroy", -> + $el.off() + return {link:link} module.directive("tgChangePasswordFromRecovery", ["$tgAuth", "$tgConfirm", "$tgLocation", "$routeParams", - "$tgNavUrls", "$translate", ChangePasswordFromRecoveryDirective]) + "$tgNavUrls", "$translate", + ChangePasswordFromRecoveryDirective]) + ############################################################################# ## Invitation @@ -365,7 +403,9 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics onSuccessSubmitLogin = (response) -> $analytics.trackEvent("auth", "invitationAccept", "invitation accept with existing user", 1) $location.path($navUrls.resolve("project", {project: $scope.invitation.project_slug})) - text = $translate.instant("INVITATION_LOGIN_FORM.SUCCESS", {"project_name": $scope.invitation.project_name}) + text = $translate.instant("INVITATION_LOGIN_FORM.SUCCESS", { + "project_name": $scope.invitation.project_name + }) $confirm.notify("success", text) @@ -388,7 +428,7 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics # Register form $scope.dataRegister = {token: token} - registerForm = $el.find("form.register-form").checksley() + registerForm = $el.find("form.register-form").checksley({onlyOneErrorElement: true}) onSuccessSubmitRegister = (response) -> $analytics.trackEvent("auth", "invitationAccept", "invitation accept with new user", 1) @@ -397,9 +437,11 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics "Welcome to #{_.escape($scope.invitation.project_name)}") onErrorSubmitRegister = (response) -> - text = $translate.instant("LOGIN_FORM.ERROR_AUTH_INCORRECT") + if response.data._error_message + text = $translate.instant("COMMON.GENERIC_ERROR", {error: response.data._error_message}) + $confirm.notify("light-error", text) - $confirm.notify("light-error", text) + registerForm.setErrors(response.data) submitRegister = debounce 2000, (event) => event.preventDefault() @@ -413,11 +455,15 @@ InvitationDirective = ($auth, $confirm, $location, $params, $navUrls, $analytics $el.on "submit", "form.register-form", submitRegister $el.on "click", ".button-register", submitRegister + $scope.$on "$destroy", -> + $el.off() + return {link:link} module.directive("tgInvitation", ["$tgAuth", "$tgConfirm", "$tgLocation", "$routeParams", "$tgNavUrls", "$tgAnalytics", "$translate", InvitationDirective]) + ############################################################################# ## Change Email ############################################################################# @@ -429,12 +475,15 @@ ChangeEmailDirective = ($repo, $model, $auth, $confirm, $location, $params, $nav form = $el.find("form").checksley() onSuccessSubmit = (response) -> - $repo.queryOne("users", $auth.getUser().id).then (data) => - $auth.setUser(data) - $location.path($navUrls.resolve("home")) + if $auth.isAuthenticated() + $repo.queryOne("users", $auth.getUser().id).then (data) => + $auth.setUser(data) + $location.path($navUrls.resolve("home")) + else + $location.path($navUrls.resolve("login")) - text = $translate.instant("CHANGE_EMAIL_FORM.SUCCESS") - $confirm.success(text) + text = $translate.instant("CHANGE_EMAIL_FORM.SUCCESS") + $confirm.success(text) onErrorSubmit = (response) -> text = $translate.instant("COMMON.GENERIC_ERROR", {error: response.data._error_message}) @@ -456,10 +505,14 @@ ChangeEmailDirective = ($repo, $model, $auth, $confirm, $location, $params, $nav event.preventDefault() submit() + $scope.$on "$destroy", -> + $el.off() + return {link:link} -module.directive("tgChangeEmail", ["$tgRepo", "$tgModel", "$tgAuth", "$tgConfirm", "$tgLocation", "$routeParams", - "$tgNavUrls", "$translate", ChangeEmailDirective]) +module.directive("tgChangeEmail", ["$tgRepo", "$tgModel", "$tgAuth", "$tgConfirm", "$tgLocation", + "$routeParams", "$tgNavUrls", "$translate", ChangeEmailDirective]) + ############################################################################# ## Cancel account @@ -495,7 +548,10 @@ CancelAccountDirective = ($repo, $model, $auth, $confirm, $location, $params, $n $el.on "submit", "form", submit + $scope.$on "$destroy", -> + $el.off() + return {link:link} -module.directive("tgCancelAccount", ["$tgRepo", "$tgModel", "$tgAuth", "$tgConfirm", "$tgLocation", "$routeParams", - "$tgNavUrls", CancelAccountDirective]) +module.directive("tgCancelAccount", ["$tgRepo", "$tgModel", "$tgAuth", "$tgConfirm", "$tgLocation", + "$routeParams","$tgNavUrls", CancelAccountDirective]) diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee index 27ce413c..845f33a7 100644 --- a/app/coffee/modules/backlog/main.coffee +++ b/app/coffee/modules/backlog/main.coffee @@ -45,16 +45,15 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F "$routeParams", "$q", "$tgLocation", - "$appTitle", + "tgAppMetaService", "$tgNavUrls", "$tgEvents", "$tgAnalytics", - "tgLoader", "$translate" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, - @location, @appTitle, @navUrls, @events, @analytics, tgLoader, @translate) -> + @location, @appMetaService, @navUrls, @events, @analytics, @translate) -> bindMethods(@) @scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME") @@ -67,7 +66,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # On Success promise.then => - @appTitle.set("Backlog - " + @scope.project.name) + title = @translate.instant("BACKLOG.PAGE_TITLE", {projectName: @scope.project.name}) + description = @translate.instant("BACKLOG.PAGE_DESCRIPTION", { + projectName: @scope.project.name, + projectDescription: @scope.project.description + }) + @appMetaService.setAll(title, description) if @rs.userstories.getShowTags(@scope.projectId) @showTags = true @@ -77,9 +81,6 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F # On Error promise.then null, @.onInitialDataError.bind(@) - # Finally - promise.finally tgLoader.pageLoaded - initializeEventHandlers: -> @scope.$on "usform:bulk:success", => @.loadUserstories() @@ -607,9 +608,11 @@ BacklogDirective = ($repo, $rootscope, $translate) -> $ctrl.loadProjectStats() - # Enable move to current sprint only when there are selected us's - $el.on "change", ".backlog-table-body .user-stories input:checkbox", (event) -> - target = angular.element(event.currentTarget) + shiftPressed = false + lastChecked = null + + checkSelected = (target) -> + lastChecked = target.closest(".us-item-row") moveToCurrentSprintDom = $el.find("#move-to-current-sprint") selectedUsDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") @@ -620,6 +623,33 @@ BacklogDirective = ($repo, $rootscope, $translate) -> target.closest('.us-item-row').toggleClass('ui-multisortable-multiple') + $(window).on "keydown.shift-pressed keyup.shift-pressed", (event) -> + shiftPressed = !!event.shiftKey + + return true + + # Enable move to current sprint only when there are selected us's + $el.on "change", ".backlog-table-body .user-stories input:checkbox", (event) -> + # check elements between the last two if shift is pressed + if lastChecked && shiftPressed + elements = [] + current = $(event.currentTarget).closest(".us-item-row") + nextAll = lastChecked.nextAll() + prevAll = lastChecked.prevAll() + + if _.some(nextAll, (next) -> next == current[0]) + elements = lastChecked.nextUntil(current) + else if _.some(prevAll, (prev) -> prev == current[0]) + elements = lastChecked.prevUntil(current) + + _.map elements, (elm) -> + input = $(elm).find("input:checkbox") + input.prop('checked', true); + checkSelected(input) + + target = angular.element(event.currentTarget) + checkSelected(target) + $el.on "click", "#move-to-current-sprint", (event) => # Calculating the us's to be modified ussDom = $el.find(".backlog-table-body .user-stories input:checkbox:checked") @@ -705,6 +735,7 @@ BacklogDirective = ($repo, $rootscope, $translate) -> $scope.$on "$destroy", -> $el.off() + $(window).off(".shift-pressed") return {link: link} diff --git a/app/coffee/modules/backlog/sortable.coffee b/app/coffee/modules/backlog/sortable.coffee index 48d9eea2..70b25dbf 100644 --- a/app/coffee/modules/backlog/sortable.coffee +++ b/app/coffee/modules/backlog/sortable.coffee @@ -61,10 +61,10 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> items: ".us-item-row", cancel: ".popover" connectWith: ".sprint" - containment: ".wrapper" dropOnEmpty: true placeholder: "row us-item-row us-item-drag sortable-placeholder" scroll: true + disableHorizontalScroll: true # A consequence of length of backlog user story item # the default tolerance ("intersection") not works properly. tolerance: "pointer" @@ -73,8 +73,11 @@ BacklogSortableDirective = ($repo, $rs, $rootscope, $tgConfirm, $translate) -> # works unexpectly (in some circumstances calculates wrong # position for revert). revert: false - cursorAt: {right: 15} + start: () -> + $(document.body).addClass("drag-active") stop: () -> + $(document.body).removeClass("drag-active") + if $el.hasClass("active-filters") $el.sortable("cancel") filterError() @@ -167,8 +170,11 @@ SprintSortableDirective = ($repo, $rs, $rootscope) -> $el.sortable({ scroll: true dropOnEmpty: true - items: ".sprint-table .milestone-us-item-row", + items: ".sprint-table .milestone-us-item-row" + disableHorizontalScroll: true connectWith: ".sprint,.backlog-table-body,.empty-backlog" + placeholder: "row us-item-row sortable-placeholder" + forcePlaceholderSize:true }) $el.on "multiplesortreceive", (event, ui) -> diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index 8bdf9917..dbd56c0e 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -44,6 +44,7 @@ module.directive("tgMain", ["$rootScope", "$window", TaigaMainDirective]) urls = { "home": "/" + "projects": "/projects" "error": "/error" "not-found": "/not-found" "permission-denied": "/permission-denied" @@ -57,7 +58,8 @@ urls = { "invitation": "/invitation/:token" "create-project": "/create-project" - "profile": "/:user" + "profile": "/profile" + "user-profile": "/profile/:username" "project": "/project/:project" "project-backlog": "/project/:project/backlog" @@ -67,9 +69,7 @@ urls = { "project-search": "/project/:project/search" "project-userstories-detail": "/project/:project/us/:ref" - "project-tasks-detail": "/project/:project/task/:ref" - "project-issues-detail": "/project/:project/issue/:ref" "project-wiki": "/project/:project/wiki" @@ -102,10 +102,10 @@ urls = { "project-admin-contrib": "/project/:project/admin/contrib/:plugin" # User settings - "user-settings-user-profile": "/project/:project/user-settings/user-profile" - "user-settings-user-change-password": "/project/:project/user-settings/user-change-password" - "user-settings-user-avatar": "/project/:project/user-settings/user-avatar" - "user-settings-mail-notifications": "/project/:project/user-settings/mail-notifications" + "user-settings-user-profile": "/user-settings/user-profile" + "user-settings-user-change-password": "/user-settings/user-change-password" + "user-settings-user-avatar": "/user-settings/user-avatar" + "user-settings-mail-notifications": "/user-settings/mail-notifications" } diff --git a/app/coffee/modules/base/contrib.coffee b/app/coffee/modules/base/contrib.coffee index 1c148bb9..8cb17162 100644 --- a/app/coffee/modules/base/contrib.coffee +++ b/app/coffee/modules/base/contrib.coffee @@ -22,9 +22,16 @@ taigaContribPlugins = @.taigaContribPlugins = @.taigaContribPlugins or [] class ContribController extends taiga.Controller - @.$inject = ["$rootScope", "$scope", "$routeParams", "$tgRepo", "$tgResources", "$tgConfirm", "$appTitle"] + @.$inject = [ + "$rootScope", + "$scope", + "$routeParams", + "$tgRepo", + "$tgResources", + "$tgConfirm" + ] - constructor: (@rootScope, @scope, @params, @repo, @rs, @confirm, @appTitle) -> + constructor: (@rootScope, @scope, @params, @repo, @rs, @confirm) -> @scope.adminPlugins = _.where(@rootScope.contribPlugins, {"type": "admin"}) @scope.currentPlugin = _.first(_.where(@scope.adminPlugins, {"slug": @params.plugin})) @scope.pluginTemplate = "contrib/#{@scope.currentPlugin.slug}" @@ -32,25 +39,19 @@ class ContribController extends taiga.Controller promise = @.loadInitialData() - promise.then () => - @appTitle.set(@scope.project.name) - promise.then null, => @confirm.notify("error") loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => + return @rs.projects.getBySlug(@params.pslug).then (project) => + @scope.projectId = project.id @scope.project = project @scope.$emit('project:loaded', project) @scope.$broadcast('project:loaded', project) return project loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProject()) + return @.loadProject() module = angular.module("taigaBase") module.controller("ContribController", ContribController) diff --git a/app/coffee/modules/base/http.coffee b/app/coffee/modules/base/http.coffee index e8847601..44647e6b 100644 --- a/app/coffee/modules/base/http.coffee +++ b/app/coffee/modules/base/http.coffee @@ -22,9 +22,9 @@ taiga = @.taiga class HttpService extends taiga.Service - @.$inject = ["$http", "$q", "$tgStorage", "$rootScope", "$cacheFactory"] + @.$inject = ["$http", "$q", "$tgStorage", "$rootScope", "$cacheFactory", "$translate"] - constructor: (@http, @q, @storage, @rootScope, @cacheFactory) -> + constructor: (@http, @q, @storage, @rootScope, @cacheFactory, @translate) -> super() @.cache = @cacheFactory("httpget"); @@ -37,7 +37,7 @@ class HttpService extends taiga.Service headers["Authorization"] = "Bearer #{token}" # Accept-Language - lang = @rootScope.user?.lang + lang = @translate.preferredLanguage() if lang headers["Accept-Language"] = lang diff --git a/app/coffee/modules/base/location.coffee b/app/coffee/modules/base/location.coffee index 0f94f454..8e05f02f 100644 --- a/app/coffee/modules/base/location.coffee +++ b/app/coffee/modules/base/location.coffee @@ -30,7 +30,7 @@ locationFactory = ($location, $route, $rootscope) -> return $location $location.isInCurrentRouteParams = (name, value) -> - params = _.merge($route.current.params, $location.search()) + params = $location.search() || {} return params[name] == value diff --git a/app/coffee/modules/base/navurls.coffee b/app/coffee/modules/base/navurls.coffee index 32d10e21..d3908790 100644 --- a/app/coffee/modules/base/navurls.coffee +++ b/app/coffee/modules/base/navurls.coffee @@ -111,6 +111,9 @@ NavigationUrlsDirective = ($navurls, $auth, $q, $location) -> target.attr("href", fullUrl) $el.on "click", (event) -> + if event.metaKey || event.ctrlKey + return + event.preventDefault() target = $(event.currentTarget) diff --git a/app/coffee/modules/base/repository.coffee b/app/coffee/modules/base/repository.coffee index 866435d7..0089ad3d 100644 --- a/app/coffee/modules/base/repository.coffee +++ b/app/coffee/modules/base/repository.coffee @@ -194,6 +194,21 @@ class RepositoryService extends taiga.Service result.paginatedBy = parseInt(headers["x-paginated-by"], 10) return result + queryOnePaginatedRaw: (name, id, params, options={}) -> + url = @urls.resolve(name) + url = "#{url}/#{id}" if id + httpOptions = _.merge({headers: {}}, options) + + return @http.get(url, params, httpOptions).then (data) => + headers = data.headers() + result = {} + result.data = data.data + result.count = parseInt(headers["x-pagination-count"], 10) + result.current = parseInt(headers["x-pagination-current"] or 1, 10) + result.paginatedBy = parseInt(headers["x-paginated-by"], 10) + + return result + resolve: (options) -> params = {} params.project = options.pslug if options.pslug? diff --git a/app/coffee/modules/common.coffee b/app/coffee/modules/common.coffee index 7c03560c..abe17a6c 100644 --- a/app/coffee/modules/common.coffee +++ b/app/coffee/modules/common.coffee @@ -138,17 +138,6 @@ ToggleCommentDirective = () -> module.directive("tgToggleComment", ToggleCommentDirective) -############################################################################# -## Set the page title -############################################################################# - -AppTitle = () -> - set = (text) -> - $("title").text(text) - - return {set: set} - -module.factory("$appTitle", AppTitle) ############################################################################# ## Get the appropiate section url for a project diff --git a/app/coffee/modules/common/analytics.coffee b/app/coffee/modules/common/analytics.coffee index 9e619473..703de053 100644 --- a/app/coffee/modules/common/analytics.coffee +++ b/app/coffee/modules/common/analytics.coffee @@ -47,7 +47,7 @@ class AnalyticsService extends taiga.Service @win.ga("require", "displayfeatures") if @.trackRoutes and (not @.ignoreFirstPageLoad) - @win.ga("send", "pageview", @.getUrl()); + @win.ga("send", "pageview", @.getUrl()) # activates page tracking if @.trackRoutes diff --git a/app/coffee/modules/common/attachments.coffee b/app/coffee/modules/common/attachments.coffee index 343635a1..6f269935 100644 --- a/app/coffee/modules/common/attachments.coffee +++ b/app/coffee/modules/common/attachments.coffee @@ -277,6 +277,8 @@ AttachmentDirective = ($template, $compile, $translate) -> if attachment.is_deprecated $el.addClass("deprecated") $el.find("input:checkbox").prop('checked', true) + else + $el.removeClass("deprecated") saveAttachment = -> attachment.description = $el.find("input[name='description']").val() diff --git a/app/coffee/modules/common/compile-html.directive.coffee b/app/coffee/modules/common/compile-html.directive.coffee new file mode 100644 index 00000000..195d6a2c --- /dev/null +++ b/app/coffee/modules/common/compile-html.directive.coffee @@ -0,0 +1,13 @@ +CompileHtmlDirective = ($compile) -> + link = (scope, element, attrs) -> + scope.$watch attrs.tgCompileHtml, (newValue, oldValue) -> + element.html(newValue) + $compile(element.contents())(scope) + + return { + link: link + } + +CompileHtmlDirective.$inject = ["$compile"] + +angular.module("taigaCommon").directive("tgCompileHtml", CompileHtmlDirective) diff --git a/app/coffee/modules/common/components.coffee b/app/coffee/modules/common/components.coffee index cbd96abc..9de5b456 100644 --- a/app/coffee/modules/common/components.coffee +++ b/app/coffee/modules/common/components.coffee @@ -220,7 +220,7 @@ WatchersDirective = ($rootscope, $confirm, $repo, $qqueue, $template, $compile, $confirm.notify("success") watchers = _.map(watchers, (watcherId) -> $scope.usersById[watcherId]) renderWatchers(watchers) - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") promise.then null, -> $model.$modelValue.revert() @@ -235,7 +235,7 @@ WatchersDirective = ($rootscope, $confirm, $repo, $qqueue, $template, $compile, $confirm.notify("success") watchers = _.map(item.watchers, (watcherId) -> $scope.usersById[watcherId]) renderWatchers(watchers) - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") promise.then null, -> item.revert() $confirm.notify("error") @@ -321,7 +321,7 @@ AssignedToDirective = ($rootscope, $confirm, $repo, $loading, $qqueue, $template $loading.finish($el) $confirm.notify("success") renderAssignedTo($model.$modelValue) - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") promise.then null, -> $model.$modelValue.revert() $confirm.notify("error") @@ -474,6 +474,10 @@ EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading, $qqueue, $tem link = ($scope, $el, $attrs, $model) -> + $scope.$on "object:updated", () -> + $el.find('.edit-subject').hide() + $el.find('.view-subject').show() + isEditable = -> return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 @@ -485,7 +489,7 @@ EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading, $qqueue, $tem promise = $repo.save($model.$modelValue) promise.then -> $confirm.notify("success") - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") $el.find('.edit-subject').hide() $el.find('.view-subject').show() promise.then null, -> @@ -501,7 +505,9 @@ EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading, $qqueue, $tem $el.find('.view-subject').hide() $el.find('input').focus() - $el.on "click", ".save", -> + $el.on "click", ".save", (e) -> + e.preventDefault() + subject = $scope.item.subject save(subject) @@ -553,6 +559,10 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading, $el.find('.edit-description').hide() $el.find('.view-description .edit').hide() + $scope.$on "object:updated", () -> + $el.find('.edit-description').hide() + $el.find('.view-description').show() + isEditable = -> return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1 @@ -563,7 +573,7 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading, promise = $repo.save($model.$modelValue) promise.then -> $confirm.notify("success") - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") $el.find('.edit-description').hide() $el.find('.view-description').show() promise.then null, -> @@ -782,9 +792,7 @@ module.directive("tgProgressBar", ["$tgTemplate", TgProgressBarDirective]) TgMainTitleDirective = ($translate) -> link = ($scope, $el, $attrs) -> $attrs.$observe "i18nSectionName", (i18nSectionName) -> - trans = $translate(i18nSectionName) - trans.then (sectionName) -> $scope.sectionName = sectionName - trans.catch (sectionName) -> $scope.sectionName = sectionName + $scope.sectionName = $translate.instant(i18nSectionName) $scope.$on "$destroy", -> $el.off() diff --git a/app/coffee/modules/common/estimation.coffee b/app/coffee/modules/common/estimation.coffee index 18348762..b6ff0f5d 100644 --- a/app/coffee/modules/common/estimation.coffee +++ b/app/coffee/modules/common/estimation.coffee @@ -92,7 +92,7 @@ UsEstimationDirective = ($tgEstimationsService, $rootScope, $repo, $confirm, $qq estimationProcess = $tgEstimationsService.create($el, us, $scope.project) estimationProcess.onSelectedPointForRole = (roleId, pointId) -> @save(roleId, pointId).then -> - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") estimationProcess.render = () -> ctx = { diff --git a/app/coffee/modules/common/history.coffee b/app/coffee/modules/common/history.coffee index 4dff84db..dc491dc4 100644 --- a/app/coffee/modules/common/history.coffee +++ b/app/coffee/modules/common/history.coffee @@ -276,8 +276,9 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c deleteCommentUser: comment.delete_comment_user.name deleteComment: comment.comment_html activityId: comment.id - canRestoreComment: (comment.delete_comment_user.pk == $scope.user.id or - $scope.project.my_permissions.indexOf("modify_project") > -1) + canRestoreComment: ($scope.user and + (comment.delete_comment_user.pk == $scope.user.id or + $scope.project.my_permissions.indexOf("modify_project") > -1)) }) html = $compile(html)($scope) @@ -371,15 +372,14 @@ HistoryDirective = ($log, $loading, $qqueue, $template, $confirm, $translate, $c $scope.$watch("comments", renderComments) $scope.$watch("history", renderActivity) - $scope.$on("history:reload", -> $ctrl.loadHistory(type, objectId)) + $scope.$on("object:updated", -> $ctrl.loadHistory(type, objectId)) # Events - $el.on "click", ".add-comment a.button-green", debounce 2000, (event) -> + $el.on "click", ".add-comment input.button-green", debounce 2000, (event) -> event.preventDefault() target = angular.element(event.currentTarget) - save(target) $el.on "click", ".show-more", (event) -> diff --git a/app/coffee/modules/common/lightboxes.coffee b/app/coffee/modules/common/lightboxes.coffee index f4d089e9..c48c7300 100644 --- a/app/coffee/modules/common/lightboxes.coffee +++ b/app/coffee/modules/common/lightboxes.coffee @@ -41,11 +41,12 @@ class LightboxService extends taiga.Service $el.css('display', 'flex') - $el.find('input,textarea').first().focus() - @animationFrame.add => $el.addClass("open") + @animationFrame.add -> + $el.find('input,textarea').first().focus() + @animationFrame.add => lightboxContent.show() defered.resolve() @@ -67,6 +68,11 @@ class LightboxService extends taiga.Service $el.addClass('close') + if $el.hasClass("remove-on-close") + scope = $el.data("scope") + scope.$destroy() + $el.remove() + closeAll: -> docEl = angular.element(document) for lightboxEl in docEl.find(".lightbox.open") @@ -148,14 +154,14 @@ module.directive("lightbox", ["lightboxService", LightboxDirective]) BlockLightboxDirective = ($rootscope, $tgrepo, $confirm, lightboxService, $loading, $qqueue, $translate) -> link = ($scope, $el, $attrs, $model) -> - $translate($attrs.title).then (title) -> - $el.find("h2.title").text(title) + title = $translate.instant($attrs.title) + $el.find("h2.title").text(title) unblock = $qqueue.bindAdd (item, finishCallback) => promise = $tgrepo.save(item) promise.then -> $confirm.notify("success") - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") $model.$setViewValue(item) finishCallback() @@ -177,7 +183,7 @@ BlockLightboxDirective = ($rootscope, $tgrepo, $confirm, lightboxService, $loadi promise = $tgrepo.save($model.$modelValue) promise.then -> $confirm.notify("success") - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") promise.then null, -> $confirm.notify("error") @@ -467,7 +473,6 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic html = $compile(html)($scope) $el.find("div.watchers").html(html) - lightboxKeyboardNavigationService.init($el) closeLightbox = () -> lightboxKeyboardNavigationService.stop() @@ -481,7 +486,7 @@ AssignedToLightboxDirective = (lightboxService, lightboxKeyboardNavigationServic render(selectedUser) lightboxService.open($el).then -> $el.find('input').focus() - + lightboxKeyboardNavigationService.init($el) $scope.$watch "usersSearch", (searchingText) -> if searchingText? @@ -562,7 +567,6 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS html = usersTemplate(ctx) $el.find("div.watchers").html(html) - lightboxKeyboardNavigationService.init($el) closeLightbox = () -> lightboxKeyboardNavigationService.stop() @@ -576,7 +580,7 @@ WatchersLightboxDirective = ($repo, lightboxService, lightboxKeyboardNavigationS lightboxService.open($el).then -> $el.find("input").focus() - lightboxKeyboardNavigationService.init($el) + lightboxKeyboardNavigationService.init($el) $scope.$watch "usersSearch", (searchingText) -> if not searchingText? diff --git a/app/coffee/modules/common/loader.coffee b/app/coffee/modules/common/loader.coffee index df14802d..4ebc0215 100644 --- a/app/coffee/modules/common/loader.coffee +++ b/app/coffee/modules/common/loader.coffee @@ -40,78 +40,84 @@ LoaderDirective = (tgLoader, $rootscope) -> $(document.body).removeClass("loader-active") $el.removeClass("active") - $rootscope.$on "$routeChangeSuccess", (e) -> - tgLoader.startCurrentPageLoader() - - $rootscope.$on "$locationChangeSuccess", (e) -> - tgLoader.reset() - return { link: link } module.directive("tgLoader", ["tgLoader", "$rootScope", LoaderDirective]) -Loader = () -> - forceDisabled = false - - defaultConfig = { - enabled: false, +Loader = ($rootscope) -> + config = { minTime: 300 } - config = _.merge({}, defaultConfig) + open = false + startLoadTime = 0 + requestCount = 0 + lastResponseDate = 0 - @.add = () -> - return () -> - if !forceDisabled - config.enabled = true + pageLoaded = (force = false) -> + if startLoadTime + timeoutValue = 0 + + if !force + endTime = new Date().getTime() + diff = endTime - startLoadTime + + if diff < config.minTime + timeoutValue = config.minTime - diff + + timeout timeoutValue, -> + $rootscope.$broadcast("loader:end") + open = false + window.prerenderReady = true # Needed by Prerender Server - @.$get = ["$rootScope", ($rootscope) -> startLoadTime = 0 + requestCount = 0 + lastResponseDate = 0 - reset = () -> - config = _.merge({}, defaultConfig) + autoClose = () -> + maxAuto = 5000 + timeoutAuto = setTimeout (() -> + pageLoaded() - pageLoaded = (force = false) -> - if startLoadTime - timeoutValue = 0 + clearInterval(intervalAuto) + ), maxAuto - if !force - endTime = new Date().getTime() - diff = endTime - startLoadTime + intervalAuto = setInterval (() -> + if lastResponseDate && requestCount == 0 + pageLoaded() - if diff < config.minTime - timeoutValue = config.minTime - diff + clearInterval(intervalAuto) + clearTimeout(timeoutAuto) + ), 50 - timeout(timeoutValue, -> $rootscope.$broadcast("loader:end")) + start = () -> + startLoadTime = new Date().getTime() + $rootscope.$broadcast("loader:start") + open = true - start = () -> - startLoadTime = new Date().getTime() - $rootscope.$broadcast("loader:start") + return { + pageLoaded: pageLoaded + start: (auto=false) -> + if !open + start() + autoClose() if auto + onStart: (fn) -> + $rootscope.$on("loader:start", fn) - return { - reset: reset - pageLoaded: pageLoaded - start: start - startCurrentPageLoader: () -> - if config.enabled - start() + onEnd: (fn) -> + $rootscope.$on("loader:end", fn) - onStart: (fn) -> - $rootscope.$on("loader:start", fn) + logRequest: () -> + requestCount++ - onEnd: (fn) -> - $rootscope.$on("loader:end", fn) + logResponse: () -> + requestCount-- + lastResponseDate = new Date().getTime() + } - preventLoading: () -> - forceDisabled = true - disablePreventLoading: () -> - forceDisabled = false - } - ] +Loader.$inject = ["$rootScope"] - return - -module.provider("tgLoader", [Loader]) +module.factory("tgLoader", Loader) diff --git a/app/coffee/modules/common/tags.coffee b/app/coffee/modules/common/tags.coffee index e915fd84..ee655f32 100644 --- a/app/coffee/modules/common/tags.coffee +++ b/app/coffee/modules/common/tags.coffee @@ -285,7 +285,7 @@ TagLineDirective = ($rootScope, $repo, $rs, $confirm, $qqueue, $template, $compi $model.$setViewValue(model) onSuccess = -> - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") onError = -> $confirm.notify("error") model.revert() @@ -306,7 +306,7 @@ TagLineDirective = ($rootScope, $repo, $rs, $confirm, $qqueue, $template, $compi $model.$setViewValue(model) onSuccess = -> - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") onError = -> $confirm.notify("error") model.revert() diff --git a/app/coffee/modules/feedback.coffee b/app/coffee/modules/feedback.coffee index 403b46da..247db488 100644 --- a/app/coffee/modules/feedback.coffee +++ b/app/coffee/modules/feedback.coffee @@ -29,7 +29,7 @@ trim = @.taiga.trim module = angular.module("taigaFeedback", []) -FeedbackDirective = ($lightboxService, $repo, $confirm, $loading)-> +FeedbackDirective = ($lightboxService, $repo, $confirm, $loading, feedbackService)-> link = ($scope, $el, $attrs) -> form = $el.find("form").checksley() @@ -56,16 +56,23 @@ FeedbackDirective = ($lightboxService, $repo, $confirm, $loading)-> $el.on "submit", "form", submit - $scope.$on "feedback:show", -> - $scope.$apply -> - $scope.feedback = {} - + openLightbox = -> + $scope.feedback = {} $lightboxService.open($el) $el.find("textarea").focus() $scope.$on "$destroy", -> $el.off() - return {link:link} + openLightbox() -module.directive("tgLbFeedback", ["lightboxService", "$tgRepo", "$tgConfirm", "$tgLoading", FeedbackDirective]) + directive = { + link: link, + templateUrl: "common/lightbox-feedback.html" + scope: {} + } + + return directive + +module.directive("tgLbFeedback", ["lightboxService", "$tgRepo", "$tgConfirm", + "$tgLoading", "tgFeedbackService", FeedbackDirective]) diff --git a/app/coffee/modules/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee index db633dc2..36bb542e 100644 --- a/app/coffee/modules/issues/detail.coffee +++ b/app/coffee/modules/issues/detail.coffee @@ -44,15 +44,14 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$q", "$tgLocation", "$log", - "$appTitle", + "tgAppMetaService", "$tgAnalytics", "$tgNavUrls", - "$translate", - "tgLoader" + "$translate" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @log, @appTitle, @analytics, @navUrls, @translate, tgLoader) -> + @log, @appMetaService, @analytics, @navUrls, @translate) -> @scope.issueRef = @params.issueref @scope.sectionName = @translate.instant("ISSUES.SECTION_NAME") @.initializeEventHandlers() @@ -61,33 +60,45 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin) # On Success promise.then => - @appTitle.set(@scope.issue.subject + " - " + @scope.project.name) + @._setMeta() @.initializeOnDeleteGoToUrl() # On Error promise.then null, @.onInitialDataError.bind(@) - # Finally - promise.finally tgLoader.pageLoaded + _setMeta: -> + title = @translate.instant("ISSUE.PAGE_TITLE", { + issueRef: "##{@scope.issue.ref}" + issueSubject: @scope.issue.subject + projectName: @scope.project.name + }) + description = @translate.instant("ISSUE.PAGE_DESCRIPTION", { + issueStatus: @scope.statusById[@scope.issue.status]?.name or "--" + issueType: @scope.typeById[@scope.issue.type]?.name or "--" + issueSeverity: @scope.severityById[@scope.issue.severity]?.name or "--" + issuePriority: @scope.priorityById[@scope.issue.priority]?.name or "--" + issueDescription: angular.element(@scope.issue.description_html or "").text() + }) + @appMetaService.setAll(title, description) initializeEventHandlers: -> @scope.$on "attachment:create", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @analytics.trackEvent("attachment", "create", "create attachment on issue", 1) @scope.$on "attachment:edit", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "attachment:delete", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "promote-issue-to-us:success", => @analytics.trackEvent("issue", "promoteToUserstory", "promote issue to userstory", 1) - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @.loadIssue() @scope.$on "custom-attributes-values:edit", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") initializeOnDeleteGoToUrl: -> ctx = {project: @scope.project.slug} @@ -229,7 +240,7 @@ IssueStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $t onSuccess = -> $confirm.notify("success") $model.$setViewValue(issue) - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") $loading.finish($el.find(".level-name")) onError = -> $confirm.notify("error") @@ -313,7 +324,7 @@ IssueTypeButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $tem onSuccess = -> $confirm.notify("success") $model.$setViewValue(issue) - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") $loading.finish($el.find(".level-name")) onError = -> @@ -399,7 +410,7 @@ IssueSeverityButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, onSuccess = -> $confirm.notify("success") $model.$setViewValue(issue) - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") $loading.finish($el.find(".level-name")) onError = -> $confirm.notify("error") @@ -486,7 +497,7 @@ IssuePriorityButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, onSuccess = -> $confirm.notify("success") $model.$setViewValue(issue) - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") $loading.finish($el.find(".level-name")) onError = -> $confirm.notify("error") diff --git a/app/coffee/modules/issues/list.coffee b/app/coffee/modules/issues/list.coffee index 59ff2a0d..607d2b29 100644 --- a/app/coffee/modules/issues/list.coffee +++ b/app/coffee/modules/issues/list.coffee @@ -47,18 +47,16 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi "$routeParams", "$q", "$tgLocation", - "$appTitle", + "tgAppMetaService", "$tgNavUrls", "$tgEvents", "$tgAnalytics", - "tgLoader", "$translate" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appTitle, - @navUrls, @events, @analytics, tgLoader, @translate) -> - - @scope.sectionName = @translate.instant("ISSUES.LIST_SECTION_NAME") + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appMetaService, + @navUrls, @events, @analytics, @translate) -> + @scope.sectionName = "Issues" @scope.filters = {} if _.isEmpty(@location.search()) @@ -72,14 +70,16 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi # On Success promise.then => - @appTitle.set("Issues - " + @scope.project.name) + title = @translate.instant("ISSUES.PAGE_TITLE", {projectName: @scope.project.name}) + description = @translate.instant("ISSUES.PAGE_DESCRIPTION", { + projectName: @scope.project.name, + projectDescription: @scope.project.description + }) + @appMetaService.setAll(title, description) # On Error promise.then null, @.onInitialDataError.bind(@) - # Finally - promise.finally tgLoader.pageLoaded - @scope.$on "issueform:new:success", => @analytics.trackEvent("issue", "create", "create issue on issues list", 1) @.loadIssues() @@ -440,7 +440,7 @@ module.directive("tgIssues", ["$log", "$tgLocation", "$tgTemplate", "$compile", ## Issues Filters Directive ############################################################################# -IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $translate, $compile) -> +IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $translate, $compile, $auth) -> template = $template.get("issue/issues-filters.html", true) templateSelected = $template.get("issue/issues-filters-selected.html", true) @@ -477,7 +477,7 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ html = $compile(html)($scope) $el.find(".filters-applied").html(html) - if selectedFilters.length > 0 + if $auth.isAuthenticated() && selectedFilters.length > 0 $el.find(".save-filters").show() else $el.find(".save-filters").hide() @@ -663,7 +663,7 @@ IssuesFiltersDirective = ($log, $location, $rs, $confirm, $loading, $template, $ return {link:link} module.directive("tgIssuesFilters", ["$log", "$tgLocation", "$tgResources", "$tgConfirm", "$tgLoading", - "$tgTemplate", "$translate", "$compile", IssuesFiltersDirective]) + "$tgTemplate", "$translate", "$compile", "$tgAuth", IssuesFiltersDirective]) ############################################################################# @@ -719,6 +719,7 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) -> $scope.$apply () -> $repo.save(issue).then -> + $ctrl.loadIssues() for filter in $scope.filters.statuses if filter.id == issue.status @@ -726,21 +727,6 @@ IssueStatusInlineEditionDirective = ($repo, $template, $rootscope) -> $rootscope.$broadcast("filters:issueupdate", $scope.filters) - filtering = false - - for filter in $scope.filters.statuses - if filter.selected == true - filtering = true - if filter.id == issue.status - return - - if not filtering - return - - for el, i in $scope.issues - if el and el.id == issue.id - $scope.issues.splice(i, 1) - for filter in $scope.filters.statuses if filter.id == issue.status filter.count++ diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee index 6b45ea6b..137521c6 100644 --- a/app/coffee/modules/kanban/main.coffee +++ b/app/coffee/modules/kanban/main.coffee @@ -58,16 +58,15 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi "$routeParams", "$q", "$tgLocation", - "$appTitle", + "tgAppMetaService", "$tgNavUrls", "$tgEvents", "$tgAnalytics", - "tgLoader", "$translate" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @appTitle, @navUrls, @events, @analytics, tgLoader, @translate) -> + @appMetaService, @navUrls, @events, @analytics, @translate) -> bindMethods(@) @@ -79,14 +78,16 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi # On Success promise.then => - @appTitle.set("Kanban - " + @scope.project.name) + title = @translate.instant("KANBAN.PAGE_TITLE", {projectName: @scope.project.name}) + description = @translate.instant("KANBAN.PAGE_DESCRIPTION", { + projectName: @scope.project.name, + projectDescription: @scope.project.description + }) + @appMetaService.setAll(title, description) # On Error promise.then null, @.onInitialDataError.bind(@) - # Finally - promise.finally tgLoader.pageLoaded - initializeEventHandlers: -> @scope.$on "usform:new:success", => @.loadUserstories() diff --git a/app/coffee/modules/nav.coffee b/app/coffee/modules/nav.coffee deleted file mode 100644 index 8c34d3ee..00000000 --- a/app/coffee/modules/nav.coffee +++ /dev/null @@ -1,316 +0,0 @@ -### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# File: modules/nav.coffee -### - -taiga = @.taiga -groupBy = @.taiga.groupBy -bindOnce = @.taiga.bindOnce -timeout = @.taiga.timeout - -module = angular.module("taigaNavMenu", []) - - -############################################################################# -## Projects Navigation -############################################################################# - -class ProjectsNavigationController extends taiga.Controller - @.$inject = ["$scope", "$rootScope", "$tgResources", "$tgNavUrls", "$projectUrl"] - - constructor: (@scope, @rootscope, @rs, @navurls, @projectUrl) -> - promise = @.loadInitialData() - promise.then null, -> - console.log "FAIL" - # TODO - - # Listen when someone wants to reload all the projects - @scope.$on "projects:reload", => - @.loadInitialData() - - # Listen when someone has reloaded a project - @scope.$on "project:loaded", (ctx, project) => - @.loadInitialData() - - loadInitialData: -> - return @rs.projects.listByMember(@rootscope.user?.id).then (projects) => - for project in projects - project.url = @projectUrl.get(project) - @scope.projects = projects - @scope.filteredProjects = projects - @scope.filterText = "" - return projects - - newProject: -> - @scope.$apply () => - @rootscope.$broadcast("projects:create") - - filterProjects: (text) -> - @scope.filteredProjects = _.filter @scope.projects, (project) -> - project.name.toLowerCase().indexOf(text) > -1 - - @scope.filterText = text - @rootscope.$broadcast("projects:filtered") - -module.controller("ProjectsNavigationController", ProjectsNavigationController) - - -ProjectsNavigationDirective = ($rootscope, animationFrame, $timeout, tgLoader, $location, $compile, $template) -> - baseTemplate = $template.get("project/project-navigation-base.html", true) - projectsTemplate = $template.get("project/project-navigation-list.html", true) - - overlay = $(".projects-nav-overlay") - loadingStart = 0 - - hideMenu = () -> - if overlay.is(':visible') - difftime = new Date().getTime() - loadingStart - timeoutValue = 0 - - if (difftime < 1000) - timeoutValue = 1000 - timeoutValue - - timeout timeoutValue, -> - overlay.one 'transitionend', () -> - $(document.body) - .removeClass("loading-project open-projects-nav closed-projects-nav") - .css("overflow-x", "visible") - - overlay.hide() - - $(document.body).addClass("closed-projects-nav") - - tgLoader.disablePreventLoading() - - - link = ($scope, $el, $attrs, $ctrls) -> - $ctrl = $ctrls[0] - $rootscope.$on("project:loaded", hideMenu) - - renderProjects = (projects) -> - html = projectsTemplate({projects: projects}) - $el.find(".projects-list").html(html) - - $scope.$emit("regenerate:project-pagination") - - render = (projects) -> - $el.html($compile(baseTemplate())($scope)) - renderProjects(projects) - - overlay.on 'click', () -> - hideMenu() - - $(document).on 'keydown', (e) => - code = if e.keyCode then e.keyCode else e.which - if code == 27 - hideMenu() - - $scope.$on "nav:projects-list:open", -> - if !$(document.body).hasClass("open-projects-nav") - animationFrame.add () => overlay.show() - - animationFrame.add( - () => $(document.body).css("overflow-x", "hidden") - () => $(document.body).toggleClass("open-projects-nav") - ) - - $el.on "click", ".projects-list > li > a", (event) -> - # HACK: to solve a problem with the loader when the next url - # is equal to the current one - target = angular.element(event.currentTarget) - nextUrl = target.prop("href") - currentUrl = $location.absUrl() - if nextUrl == currentUrl - hideMenu() - return - # END HACK - - $(document.body).addClass('loading-project') - - tgLoader.preventLoading() - - loadingStart = new Date().getTime() - - $el.on "click", ".create-project-button", (event) -> - event.preventDefault() - $ctrl.newProject() - - $el.on "keyup", ".search-project", (event) -> - target = angular.element(event.currentTarget) - $ctrl.filterProjects(target.val()) - - $scope.$on "projects:filtered", -> - renderProjects($scope.filteredProjects) - - $scope.$watch "projects", (projects) -> - render(projects) if projects? - - return { - require: ["tgProjectsNav"] - controller: ProjectsNavigationController - link: link - } - - -module.directive("tgProjectsNav", ["$rootScope", "animationFrame", "$timeout", "tgLoader", "$tgLocation", "$compile", - "$tgTemplate", ProjectsNavigationDirective]) - - -############################################################################# -## Project -############################################################################# - -ProjectMenuDirective = ($log, $compile, $auth, $rootscope, $tgAuth, $location, $navUrls, $config, $template) -> - menuEntriesTemplate = $template.get("project/project-menu.html", true) - - mainTemplate = _.template(""" - - - """) - - # If the last page was kanban or backlog and - # the new one is the task detail or the us details - # this method preserve the last section name. - getSectionName = ($el, sectionName, project) -> - oldSectionName = $el.find("a.active").parent().attr("id")?.replace("nav-", "") - - if sectionName == "backlog-kanban" - if oldSectionName in ["backlog", "kanban"] - sectionName = oldSectionName - else if project.is_backlog_activated && !project.is_kanban_activated - sectionName = "backlog" - else if !project.is_backlog_activated && project.is_kanban_activated - sectionName = "kanban" - - return sectionName - - renderMainMenu = ($el) -> - html = mainTemplate({}) - $el.html(html) - - # WARNING: this code has traces of slighty hacky parts - # This rerenders and compiles the navigation when ng-view - # content loaded signal is raised using inner scope. - renderMenuEntries = ($el, targetScope, project={}) -> - container = $el.find(".menu-container") - sectionName = getSectionName($el, targetScope.section, project) - - ctx = { - user: $auth.getUser(), - project: project, - feedbackEnabled: $config.get("feedbackEnabled") - } - dom = $compile(menuEntriesTemplate(ctx))(targetScope) - - dom.find("a.active").removeClass("active") - dom.find("#nav-#{sectionName} > a").addClass("active") - - container.replaceWith(dom) - - videoConferenceUrl = (project) -> - urlFixer = (url) -> return url - - if project.videoconferences == "appear-in" - baseUrl = "https://appear.in/" - else if project.videoconferences == "talky" - baseUrl = "https://talky.io/" - else if project.videoconferences == "jitsi" - baseUrl = "https://meet.jit.si/" - urlFixer = (url) -> return url.replace(/ /g, "").replace(/-/g, "") - else - return "" - - if project.videoconferences_salt - url = "#{project.slug}-#{project.videoconferences_salt}" - else - url = "#{project.slug}" - - url = urlFixer(url) - - return baseUrl + url - - - link = ($scope, $el, $attrs, $ctrl) -> - renderMainMenu($el) - project = null - - $el.on "click", ".logo", (event) -> - event.preventDefault() - target = angular.element(event.currentTarget) - $rootscope.$broadcast("nav:projects-list:open") - - $el.on "click", ".user-settings .avatar", (event) -> - event.preventDefault() - $el.find(".user-settings .popover").popover().open() - - $el.on "click", ".logout", (event) -> - event.preventDefault() - $auth.logout() - $scope.$apply -> - $location.path($navUrls.resolve("login")) - - $el.on "click", "#nav-search > a", (event) -> - event.preventDefault() - $rootscope.$broadcast("search-box:show", project) - - $el.on "click", ".feedback", (event) -> - event.preventDefault() - $rootscope.$broadcast("feedback:show") - - $scope.$on "projects:loaded", (listener) -> - $el.addClass("hidden") - listener.stopPropagation() - - $scope.$on "project:loaded", (ctx, newProject) -> - project = newProject - if $el.hasClass("hidden") - $el.removeClass("hidden") - - project.videoconferenceUrl = videoConferenceUrl(project) - renderMenuEntries($el, ctx.targetScope, project) - - return {link: link} - -module.directive("tgProjectMenu", ["$log", "$compile", "$tgAuth", "$rootScope", "$tgAuth", "$tgLocation", - "$tgNavUrls", "$tgConfig", "$tgTemplate", ProjectMenuDirective]) diff --git a/app/coffee/modules/projects/lightboxes.coffee b/app/coffee/modules/projects/lightboxes.coffee index e83d328b..1c4eac5c 100644 --- a/app/coffee/modules/projects/lightboxes.coffee +++ b/app/coffee/modules/projects/lightboxes.coffee @@ -26,7 +26,7 @@ debounce = @.taiga.debounce module = angular.module("taigaProject") -CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $projectUrl, $loading, lightboxService, $cacheFactory, $translate) -> +CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $projectUrl, $loading, lightboxService, $cacheFactory, $translate, currentUserService) -> link = ($scope, $el, attrs) -> $scope.data = {} $scope.templates = [] @@ -46,6 +46,7 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project $location.url($projectUrl.get(response)) lightboxService.close($el) + currentUserService._loadProjects() onErrorSubmit = (response) -> $loading.finish(submitButton) @@ -69,7 +70,7 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project promise = $repo.create("projects", $scope.data) promise.then(onSuccessSubmit, onErrorSubmit) - $scope.$on "projects:create", -> + openLightbox = -> $scope.data = { total_story_points: 100 total_milestones: 5 @@ -125,17 +126,30 @@ CreateProject = ($rootscope, $repo, $confirm, $location, $navurls, $rs, $project event.preventDefault() lightboxService.close($el) - return {link:link} + $scope.$on "$destroy", -> + $el.off() -module.directive("tgLbCreateProject", ["$rootScope", "$tgRepo", "$tgConfirm", "$location", "$tgNavUrls", - "$tgResources", "$projectUrl", "$tgLoading", "lightboxService", "$cacheFactory", "$translate", CreateProject]) + openLightbox() + + directive = { + link: link, + templateUrl: "project/wizard-create-project.html" + scope: {} + } + + return directive + + +module.directive("tgLbCreateProject", ["$rootScope", "$tgRepo", "$tgConfirm", + "$location", "$tgNavUrls", "$tgResources", "$projectUrl", "$tgLoading", + "lightboxService", "$cacheFactory", "$translate", "tgCurrentUserService", CreateProject]) ############################################################################# ## Delete Project Lightbox Directive ############################################################################# -DeleteProjectDirective = ($repo, $rootscope, $auth, $location, $navUrls, $confirm, lightboxService, tgLoader) -> +DeleteProjectDirective = ($repo, $rootscope, $auth, $location, $navUrls, $confirm, lightboxService, tgLoader, currentUserService) -> link = ($scope, $el, $attrs) -> projectToDelete = null $scope.$on "deletelightbox:new", (ctx, project)-> @@ -156,6 +170,7 @@ DeleteProjectDirective = ($repo, $rootscope, $auth, $location, $navUrls, $confir $rootscope.$broadcast("projects:reload") $location.path($navUrls.resolve("home")) $confirm.notify("success") + currentUserService._loadProjects() # FIXME: error handling? promise.then null, -> @@ -173,4 +188,4 @@ DeleteProjectDirective = ($repo, $rootscope, $auth, $location, $navUrls, $confir return {link:link} module.directive("tgLbDeleteProject", ["$tgRepo", "$rootScope", "$tgAuth", "$tgLocation", "$tgNavUrls", - "$tgConfirm", "lightboxService", "tgLoader", DeleteProjectDirective]) + "$tgConfirm", "lightboxService", "tgLoader", "tgCurrentUserService", DeleteProjectDirective]) diff --git a/app/coffee/modules/projects/main.coffee b/app/coffee/modules/projects/main.coffee deleted file mode 100644 index dafe8c41..00000000 --- a/app/coffee/modules/projects/main.coffee +++ /dev/null @@ -1,269 +0,0 @@ -### -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino Garcia -# Copyright (C) 2014 David Barragán Merino -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# File: modules/common/attachments.coffee -### - -taiga = @.taiga -module = angular.module("taigaProject") -bindOnce = @.taiga.bindOnce - -class ProjectsController extends taiga.Controller - @.$inject = [ - "$scope", - "$q", - "$tgResources", - "$rootScope", - "$tgNavUrls", - "$tgAuth", - "$tgLocation", - "$appTitle", - "$projectUrl", - "tgLoader" - ] - - constructor: (@scope, @q, @rs, @rootscope, @navUrls, @auth, @location, @appTitle, @projectUrl, - tgLoader) -> - @appTitle.set("Projects") - - if !@auth.isAuthenticated() - @location.path(@navUrls.resolve("login")) - else - tgLoader.start() - - @.user = @auth.getUser() - - @.projects = [] - promise = @.loadInitialData() - - promise.then () => - @scope.$emit("projects:loaded", @.projects) - - promise.then null, @.onInitialDataError.bind(@) - - # Finally - promise.finally tgLoader.pageLoaded - - loadInitialData: -> - return @rs.projects.listByMember(@rootscope.user?.id).then (projects) => - @.projects = {'recents': projects.slice(0, 8), 'all': projects} - for project in projects - project.url = @projectUrl.get(project) - - return projects - - newProject: -> - @rootscope.$broadcast("projects:create") - - logout: -> - @auth.logout() - @location.path(@navUrls.resolve("login")) - -module.controller("ProjectsController", ProjectsController) - - -class ProjectController extends taiga.Controller - @.$inject = [ - "$scope", - "$tgResources", - "$tgRepo", - "$routeParams", - "$q", - "$rootScope", - "$appTitle", - "$tgLocation", - "$tgNavUrls" - ] - - constructor: (@scope, @rs, @repo, @params, @q, @rootscope, @appTitle, @location, @navUrls) -> - promise = @.loadInitialData() - - promise.then () => - @appTitle.set(@scope.project.name) - @scope.$emit("regenerate:project-pagination") - - promise.then null, @.onInitialDataError.bind(@) - - loadInitialData: -> - # Resolve project slug - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadPageData()) - .then(=> @scope.$emit("project:loaded", @scope.project)) - - loadPageData: -> - return @q.all([ - @.loadProjectStats(), - @.loadProject()]) - - loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => - @scope.project = project - return project - - loadProjectStats: -> - return @rs.projects.stats(@scope.projectId).then (stats) => - @scope.stats = stats - return stats - -module.controller("ProjectController", ProjectController) - - -ProjectsPaginationDirective = ($timeout) -> - link = ($scope, $el, $attrs) -> - prevBtn = $el.find(".v-pagination-previous") - nextBtn = $el.find(".v-pagination-next") - container = $el.find("ul") - - pageSize = 0 - containerSize = 0 - - render = -> - pageSize = $el.find(".v-pagination-list").height() - - if container.find("li").length - if hasPagination() - if hasNextPage() - visible(nextBtn) - else - hide(nextBtn) - - if hasPrevPage() - visible(prevBtn) - else - hide(prevBtn) - else - remove() - else - remove() - - hasPagination = -> - containerSize = container.height() - - return containerSize > pageSize - - hasPrevPage = (top) -> - if !top? - top = -parseInt(container.css('top'), 10) || 0 - - return top != 0 - - hasNextPage = (top) -> - containerSize = container.height() - - if !top - top = -parseInt(container.css('top'), 10) || 0 - - return containerSize > pageSize && top + pageSize < containerSize - - nextPage = (callback) -> - top = parseInt(container.css('top'), 10) - newTop = top - pageSize - - lastLi = $el.find(".v-pagination-list li:last-child") - maxTop = -((lastLi.position().top + lastLi.outerHeight()) - pageSize) - - newTop = maxTop if newTop < maxTop - - container.animate({"top": newTop}, callback) - - return newTop - - prevPage = (callback) -> - top = parseInt(container.css('top'), 10) - - newTop = top + pageSize - - newTop = 0 if newTop > 0 - - container.animate({"top": newTop}, callback) - - return newTop - - visible = (element) -> - element.css('visibility', 'visible') - - hide = (element) -> - element.css('visibility', 'hidden') - - checkButtonVisibility = () -> - - remove = () -> - container.css('top', 0) - hide(prevBtn) - hide(nextBtn) - - $el.on "click", ".v-pagination-previous", (event) -> - event.preventDefault() - - if container.is(':animated') - return - - visible(nextBtn) - - newTop = prevPage() - - if !hasPrevPage(newTop) - hide(prevBtn) - - $el.on "click", ".v-pagination-next", (event) -> - event.preventDefault() - - if container.is(':animated') - return - - visible(prevBtn) - - newTop = -nextPage() - - if !hasNextPage(newTop) - hide(nextBtn) - - $scope.$on "regenerate:project-pagination", -> - remove() - render() - - $(window).on "resize.projects-pagination", render - - $scope.$on "$destroy", -> - $(window).off "resize.projects-pagination" - - return { - link: link - } - -module.directive("tgProjectsPagination", ['$timeout', ProjectsPaginationDirective]) - -ProjectsListDirective = ($compile, $template) -> - template = $template.get('project/project-list.html', true) - - link = ($scope, $el, $attrs, $ctrls) -> - render = (projects) -> - $el.html($compile(template({projects: projects}))($scope)) - $scope.$emit("regenerate:project-pagination") - - $scope.$on "projects:loaded", (ctx, projects) -> - render(projects.all) if projects.all? - - return { - link: link - } - -module.directive("tgProjectsList", ["$compile", "$tgTemplate", ProjectsListDirective]) diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index 52028c90..991133c9 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -31,13 +31,17 @@ urls = { # User "users": "/users" + "by_username": "/users/by_username" "users-password-recovery": "/users/password_recovery" "users-change-password-from-recovery": "/users/change_password_from_recovery" "users-change-password": "/users/change_password" "users-change-email": "/users/change_email" "users-cancel-account": "/users/cancel" + "contacts": "/users/%s/contacts" + "stats": "/users/%s/stats" # User - Notification + "permissions": "/permissions" "notify-policies": "/notify-policies" # User - Storage @@ -58,6 +62,7 @@ urls = { "projects": "/projects" "project-templates": "/project-templates" "project-modules": "/projects/%s/modules" + "bulk-update-projects-order": "/projects/bulk_update_order" # Project Values - Choises "userstory-statuses": "/userstory-statuses" @@ -125,6 +130,11 @@ urls = { "tasks-csv": "/tasks/csv?uuid=%s" "issues-csv": "/issues/csv?uuid=%s" + # Timeline + "timeline-profile": "/timeline/profile" + "timeline-user": "/timeline/user" + "timeline-project": "/timeline/project" + # Search "search": "/search" @@ -183,5 +193,6 @@ module.run([ "$tgWebhooksResourcesProvider", "$tgWebhookLogsResourcesProvider", "$tgLocalesResourcesProvider", + "$tgUsersResourcesProvider", initResources ]) diff --git a/app/coffee/modules/resources/issues.coffee b/app/coffee/modules/resources/issues.coffee index 0cbb99de..bf7c27df 100644 --- a/app/coffee/modules/resources/issues.coffee +++ b/app/coffee/modules/resources/issues.coffee @@ -41,6 +41,9 @@ resourceProvider = ($repo, $http, $urls, $storage, $q) -> params.ref = ref return $repo.queryOne("issues", "by_ref", params) + service.listInAllProjects = (filters) -> + return $repo.queryMany("issues", filters) + service.list = (projectId, filters, options) -> params = {project: projectId} params = _.extend({}, params, filters or {}) diff --git a/app/coffee/modules/resources/projects.coffee b/app/coffee/modules/resources/projects.coffee index ad712d42..b7243a33 100644 --- a/app/coffee/modules/resources/projects.coffee +++ b/app/coffee/modules/resources/projects.coffee @@ -37,7 +37,7 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) -> return $repo.queryMany("projects") service.listByMember = (memberId) -> - params = {"member": memberId} + params = {"member": memberId, "order_by": "memberships__user_order"} return $repo.queryMany("projects", params) service.templates = -> @@ -54,6 +54,10 @@ resourceProvider = ($config, $repo, $http, $urls, $auth, $q, $translate) -> service.stats = (projectId) -> return $repo.queryOneRaw("projects", "#{projectId}/stats") + service.bulkUpdateOrder = (bulkData) -> + url = $urls.resolve("bulk-update-projects-order") + return $http.post(url, bulkData) + service.regenerate_userstories_csv_uuid = (projectId) -> url = "#{$urls.resolve("projects")}/#{projectId}/regenerate_userstories_csv_uuid" return $http.post(url) diff --git a/app/coffee/modules/resources/tasks.coffee b/app/coffee/modules/resources/tasks.coffee index 56a00de3..7069012c 100644 --- a/app/coffee/modules/resources/tasks.coffee +++ b/app/coffee/modules/resources/tasks.coffee @@ -41,6 +41,9 @@ resourceProvider = ($repo, $http, $urls, $storage) -> params.ref = ref return $repo.queryOne("tasks", "by_ref", params) + service.listInAllProjects = (filters) -> + return $repo.queryMany("tasks", filters) + service.list = (projectId, sprintId=null, userStoryId=null) -> params = {project: projectId} params.milestone = sprintId if sprintId diff --git a/app/coffee/modules/resources/users.coffee b/app/coffee/modules/resources/users.coffee new file mode 100644 index 00000000..3db62b96 --- /dev/null +++ b/app/coffee/modules/resources/users.coffee @@ -0,0 +1,47 @@ +### +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino Garcia +# Copyright (C) 2014 David Barragán Merino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/resources/user.coffee +### + + +taiga = @.taiga +sizeFormat = @.taiga.sizeFormat + + +resourceProvider = ($http, $urls) -> + service = {} + + service.contacts = (userId, options={}) -> + url = $urls.resolve("contacts", userId) + httpOptions = {headers: {}} + + if not options.enablePagination + httpOptions.headers["x-disable-pagination"] = "1" + + return $http.get(url, {}, httpOptions) + .then (result) -> + return result.data + + return (instance) -> + instance.users = service + + +module = angular.module("taigaResources") +module.factory("$tgUsersResourcesProvider", ["$tgHttp", "$tgUrls", "$q", + resourceProvider]) diff --git a/app/coffee/modules/resources/userstories.coffee b/app/coffee/modules/resources/userstories.coffee index ac43dfff..de2f8c9b 100644 --- a/app/coffee/modules/resources/userstories.coffee +++ b/app/coffee/modules/resources/userstories.coffee @@ -38,6 +38,9 @@ resourceProvider = ($repo, $http, $urls, $storage) -> params.ref = ref return $repo.queryOne("userstories", "by_ref", params) + service.listInAllProjects = (filters) -> + return $repo.queryMany("userstories", filters) + service.listUnassigned = (projectId, filters) -> params = {"project": projectId, "milestone": "null"} params = _.extend({}, params, filters or {}) diff --git a/app/coffee/modules/search.coffee b/app/coffee/modules/search.coffee index a863f7ff..91a974a5 100644 --- a/app/coffee/modules/search.coffee +++ b/app/coffee/modules/search.coffee @@ -43,18 +43,23 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) "$routeParams", "$q", "$tgLocation", - "$appTitle", + "tgAppMetaService", "$tgNavUrls", - "tgLoader" + "$translate" ] - constructor: (@scope, @repo, @rs, @params, @q, @location, @appTitle, @navUrls, @tgLoader) -> + constructor: (@scope, @repo, @rs, @params, @q, @location, @appMetaService, @navUrls, @translate) -> @scope.sectionName = "Search" promise = @.loadInitialData() promise.then () => - @appTitle.set("Search") + title = @translate.instant("SEARCH.PAGE_TITLE", {projectName: @scope.project.name}) + description = @translate.instant("SEARCH.PAGE_DESCRIPTION", { + projectName: @scope.project.name, + projectDescription: @scope.project.description + }) + @appMetaService.setAll(title, description) promise.then null, @.onInitialDataError.bind(@) @@ -63,9 +68,7 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) loadSearchData = debounceLeading(100, (t) => @.loadSearchData(t)) @scope.$watch "searchTerm", (term) => - if not term - @tgLoader.pageLoaded() - else + if term loadSearchData(term) loadFilters: -> @@ -90,9 +93,6 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.searchResults = data return data - promise.finally => - @tgLoader.pageLoaded() - return promise loadInitialData: -> @@ -107,7 +107,7 @@ module.controller("SearchController", SearchController) ## Search box directive ############################################################################# -SearchBoxDirective = ($lightboxService, $navurls, $location, $route)-> +SearchBoxDirective = (projectService, $lightboxService, $navurls, $location, $route)-> link = ($scope, $el, $attrs) -> project = null @@ -120,24 +120,40 @@ SearchBoxDirective = ($lightboxService, $navurls, $location, $route)-> text = $el.find("#search-text").val() - url = $navurls.resolve("project-search", {project: project.slug}) + url = $navurls.resolve("project-search", {project: project.get("slug")}) - $lightboxService.close($el) $scope.$apply -> + $lightboxService.close($el) + $location.path(url) $location.search("text", text).path(url) $route.reload() - $scope.$on "search-box:show", (ctx, newProject)-> - project = newProject - $lightboxService.open($el) - $el.find("#search-text").val("") + + openLightbox = () -> + project = projectService.project + + $lightboxService.open($el).then () -> + $el.find("#search-text").focus() $el.on "submit", "form", submit - return {link:link} + openLightbox() -module.directive("tgSearchBox", ["lightboxService", "$tgNavUrls", "$tgLocation", "$route", SearchBoxDirective]) + return { + templateUrl: "search/lightbox-search.html", + link:link + } + +SearchBoxDirective.$inject = [ + "tgProjectService", + "lightboxService", + "$tgNavUrls", + "$tgLocation", + "$route" +] + +module.directive("tgSearchBox", SearchBoxDirective) ############################################################################# @@ -154,12 +170,15 @@ SearchDirective = ($log, $compile, $templatecache, $routeparams, $location) -> selectedSectionName = null selectedSectionData = null - for name, value of data - continue if name == "count" - if value.length > maxVal - maxVal = value.length - selectedSectionName = name - selectedSectionData = value + if data + for name in ["userstories", "issues", "tasks", "wikipages"] + value = data[name] + + if value.length > maxVal + maxVal = value.length + selectedSectionName = name + selectedSectionData = value + break; if maxVal == 0 return {name: "userstories", value: []} diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee index 75018b97..f4e82d92 100644 --- a/app/coffee/modules/taskboard/main.coffee +++ b/app/coffee/modules/taskboard/main.coffee @@ -44,17 +44,16 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgResources", "$routeParams", "$q", - "$appTitle", + "tgAppMetaService", "$tgLocation", "$tgNavUrls" "$tgEvents" "$tgAnalytics", - "tgLoader" "$translate" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appTitle, @location, @navUrls, - @events, @analytics, tgLoader, @translate) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appMetaService, @location, @navUrls, + @events, @analytics, @translate) -> bindMethods(@) @scope.sectionName = @translate.instant("TASKBOARD.SECTION_NAME") @@ -63,14 +62,30 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) promise = @.loadInitialData() # On Success - promise.then => - @appTitle.set("Taskboard - " + @scope.project.name) - + promise.then => @._setMeta() # On Error promise.then null, @.onInitialDataError.bind(@) - # Finally - promise.finally tgLoader.pageLoaded + _setMeta: -> + prettyDate = @translate.instant("BACKLOG.SPRINTS.DATE") + + title = @translate.instant("TASKBOARD.PAGE_TITLE", { + projectName: @scope.project.name + sprintName: @scope.sprint.name + }) + description = @translate.instant("TASKBOARD.PAGE_DESCRIPTION", { + projectName: @scope.project.name + sprintName: @scope.sprint.name + startDate: moment(@scope.sprint.estimated_start).format(prettyDate) + endDate: moment(@scope.sprint.estimated_finish).format(prettyDate) + completedPercentage: @scope.stats.completedPercentage or "0" + completedPoints: @scope.stats.completedPointsSum or "--" + totalPoints: @scope.stats.totalPointsSum or "--" + openTasks: @scope.stats.openTasks or "--" + totalTasks: @scope.stats.total_tasks or "--" + }) + + @appMetaService.setAll(title, description) initializeEventHandlers: -> # TODO: Reload entire taskboard after create/edit tasks seems @@ -136,7 +151,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.stats.remainingPointsSum = remainingPointsSum @scope.stats.remainingTasks = remainingTasks if stats.totalPointsSum - @scope.stats.completedPercentage = Math.round(100 * stats.completedPointsSum / stats.totalPointsSum) + @scope.stats.completedPercentage = Math.round(100*stats.completedPointsSum/stats.totalPointsSum) else @scope.stats.completedPercentage = 0 @@ -259,7 +274,7 @@ TaskboardDirective = ($rootscope) -> $el.on "click", ".toggle-analytics-visibility", (event) -> event.preventDefault() target = angular.element(event.currentTarget) - target.toggleClass('active'); + target.toggleClass('active') $rootscope.$broadcast("taskboard:graph:toggle-visibility") tableBodyDom = $el.find(".taskboard-table-body") diff --git a/app/coffee/modules/tasks/detail.coffee b/app/coffee/modules/tasks/detail.coffee index bd25d2f2..228c4aff 100644 --- a/app/coffee/modules/tasks/detail.coffee +++ b/app/coffee/modules/tasks/detail.coffee @@ -42,15 +42,14 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$q", "$tgLocation", "$log", - "$appTitle", + "tgAppMetaService", "$tgNavUrls", "$tgAnalytics", - "$translate", - "tgLoader" + "$translate" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @log, @appTitle, @navUrls, @analytics, @translate, tgLoader) -> + @log, @appMetaService, @navUrls, @analytics, @translate) -> @scope.taskRef = @params.taskref @scope.sectionName = @translate.instant("TASK.SECTION_NAME") @.initializeEventHandlers() @@ -58,23 +57,33 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin) promise = @.loadInitialData() promise.then () => - @appTitle.set(@scope.task.subject + " - " + @scope.project.name) + @._setMeta() @.initializeOnDeleteGoToUrl() promise.then null, @.onInitialDataError.bind(@) - promise.finally tgLoader.pageLoaded + _setMeta: -> + title = @translate.instant("TASK.PAGE_TITLE", { + taskRef: "##{@scope.task.ref}" + taskSubject: @scope.task.subject + projectName: @scope.project.name + }) + description = @translate.instant("TASK.PAGE_DESCRIPTION", { + taskStatus: @scope.statusById[@scope.task.status]?.name or "--" + taskDescription: angular.element(@scope.task.description_html or "").text() + }) + @appMetaService.setAll(title, description) initializeEventHandlers: -> @scope.$on "attachment:create", => @analytics.trackEvent("attachment", "create", "create attachment on task", 1) - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "attachment:edit", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "attachment:delete", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "custom-attributes-values:edit", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") initializeOnDeleteGoToUrl: -> ctx = {project: @scope.project.slug} @@ -169,7 +178,6 @@ TaskStatusDisplayDirective = ($template, $compile) -> }) html = $compile(html)($scope) - $el.html(html) $scope.$watch $attrs.ngModel, (task) -> @@ -241,7 +249,7 @@ TaskStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $co onSuccess = -> $confirm.notify("success") - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") $loading.finish($el.find(".level-name")) onError = -> @@ -326,7 +334,7 @@ TaskIsIocaineButtonDirective = ($rootscope, $tgrepo, $confirm, $loading, $qqueue promise.then -> $confirm.notify("success") - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") promise.then null, -> task.revert() diff --git a/app/coffee/modules/team/main.coffee b/app/coffee/modules/team/main.coffee index df13508c..6175b035 100644 --- a/app/coffee/modules/team/main.coffee +++ b/app/coffee/modules/team/main.coffee @@ -39,29 +39,30 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) "$q", "$location", "$tgNavUrls", - "$appTitle", + "tgAppMetaService", "$tgAuth", - "tgLoader", - "$translate" + "$translate", + "tgProjectService" ] - constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appTitle, @auth, tgLoader, - @translate) -> + constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appMetaService, @auth, + @translate, @projectService) -> @scope.sectionName = "TEAM.SECTION_NAME" promise = @.loadInitialData() # On Success promise.then => - text = @translate.instant("TEAM.APP_TITLE", {"projectName": @scope.project.name}) - @appTitle.set(text) + title = @translate.instant("TEAM.PAGE_TITLE", {projectName: @scope.project.name}) + description = @translate.instant("TEAM.PAGE_DESCRIPTION", { + projectName: @scope.project.name, + projectDescription: @scope.project.description + }) + @appMetaService.setAll(title, description) # On Error promise.then null, @.onInitialDataError.bind(@) - # Finally - promise.finally tgLoader.pageLoaded - setRole: (role) -> if role @scope.filtersRole = role @@ -69,27 +70,30 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.filtersRole = null loadMembers: -> - return @rs.memberships.list(@scope.projectId, {}, false).then (data) => - currentUser = @auth.getUser() - if currentUser? and not currentUser.photo? - currentUser.photo = "/images/unnamed.png" + currentUser = @auth.getUser() - @scope.currentUser = _.find data, (membership) => - return currentUser? and membership.user == currentUser.id + if currentUser? and not currentUser.photo? + currentUser.photo = "/images/unnamed.png" - @scope.totals = {} - _.forEach data, (membership) => - @scope.totals[membership.user] = 0 + memberships = @projectService.project.toJS().memberships - @scope.memberships = _.filter data, (membership) => - if membership.user && (not currentUser? or membership.user != currentUser.id) && membership.is_user_active - return membership + @scope.currentUser = _.find memberships, (membership) => + return currentUser? and membership.user == currentUser.id - for membership in @scope.memberships - if not membership.photo? - membership.photo = "/images/unnamed.png" + @scope.totals = {} - return data + _.forEach memberships, (membership) => + @scope.totals[membership.user] = 0 + + @scope.memberships = _.filter memberships, (membership) => + if membership.user && (not currentUser? or membership.user != currentUser.id) + return membership + + @scope.memberships = _.filter memberships, (membership) => return membership.is_active + + for membership in @scope.memberships + if not membership.photo? + membership.photo = "/images/unnamed.png" loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => @@ -135,7 +139,9 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin) promise = @.loadProject() return promise.then (project) => @.fillUsersAndRoles(project.users, project.roles) - return @.loadMembers().then(=> @.loadMemberStats()) + @.loadMembers() + + return @.loadMemberStats() module.controller("TeamController", TeamController) diff --git a/app/coffee/modules/user-settings/change-password.coffee b/app/coffee/modules/user-settings/change-password.coffee index 387b92d6..4ea8c65c 100644 --- a/app/coffee/modules/user-settings/change-password.coffee +++ b/app/coffee/modules/user-settings/change-password.coffee @@ -46,28 +46,11 @@ class UserChangePasswordController extends mixOf(taiga.Controller, taiga.PageMix "$translate" ] - constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth, @translate) -> + constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, + @auth, @translate) -> @scope.sectionName = @translate.instant("CHANGE_PASSWORD.SECTION_NAME") - @scope.project = {} @scope.user = @auth.getUser() - promise = @.loadInitialData() - - promise.then null, @.onInitialDataError.bind(@) - - loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => - @scope.project = project - @scope.$emit('project:loaded', project) - return project - - loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProject()) - module.controller("UserChangePasswordController", UserChangePasswordController) diff --git a/app/coffee/modules/user-settings/main.coffee b/app/coffee/modules/user-settings/main.coffee index 620236b3..151cc2dc 100644 --- a/app/coffee/modules/user-settings/main.coffee +++ b/app/coffee/modules/user-settings/main.coffee @@ -45,41 +45,33 @@ class UserSettingsController extends mixOf(taiga.Controller, taiga.PageMixin) "$translate" ] - constructor: (@scope, @rootscope, @config, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth, @translate) -> + constructor: (@scope, @rootscope, @config, @repo, @confirm, @rs, @params, @q, @location, @navUrls, + @auth, @translate) -> @scope.sectionName = "USER_SETTINGS.MENU.SECTION_TITLE" @scope.project = {} @scope.user = @auth.getUser() + + if !@scope.user + @location.path(@navUrls.resolve("permission-denied")) + @location.replace() + @scope.lang = @getLan() maxFileSize = @config.get("maxUploadFileSize", null) if maxFileSize - @translate("USER_SETTINGS.AVATAR_MAX_SIZE", {"maxFileSize": sizeFormat(maxFileSize)}).then (text) => - @scope.maxFileSizeMsg = text + text = @translate.instant("USER_SETTINGS.AVATAR_MAX_SIZE", {"maxFileSize": sizeFormat(maxFileSize)}) + @scope.maxFileSizeMsg = text promise = @.loadInitialData() promise.then null, @.onInitialDataError.bind(@) - loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => - @scope.project = project - @scope.$emit('project:loaded', project) - return project - - loadLocales: -> + loadInitialData: -> return @rs.locales.list().then (locales) => @scope.locales = locales return locales - loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return @q.all([promise.then(=> @.loadProject()), - @.loadLocales()]) - openDeleteLightbox: -> @rootscope.$broadcast("deletelightbox:new", @scope.user) diff --git a/app/coffee/modules/user-settings/notifications.coffee b/app/coffee/modules/user-settings/notifications.coffee index ca299c71..bef96385 100644 --- a/app/coffee/modules/user-settings/notifications.coffee +++ b/app/coffee/modules/user-settings/notifications.coffee @@ -46,33 +46,15 @@ class UserNotificationsController extends mixOf(taiga.Controller, taiga.PageMixi constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, @auth) -> @scope.sectionName = "USER_SETTINGS.NOTIFICATIONS.SECTION_NAME" - @scope.project = {} @scope.user = @auth.getUser() - promise = @.loadInitialData() - promise.then null, @.onInitialDataError.bind(@) - loadProject: -> - return @rs.projects.get(@scope.projectId).then (project) => - @scope.project = project - @scope.$emit('project:loaded', project) - return project - - loadNotifyPolicies: -> + loadInitialData: -> return @rs.notifyPolicies.list().then (notifyPolicies) => @scope.notifyPolicies = notifyPolicies return notifyPolicies - loadInitialData: -> - promise = @repo.resolve({pslug: @params.pslug}).then (data) => - @scope.projectId = data.project - return data - - return promise.then(=> @.loadProject()) - .then(=> @.loadNotifyPolicies()) - - module.controller("UserNotificationsController", UserNotificationsController) diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee index 79525316..935f43f3 100644 --- a/app/coffee/modules/userstories/detail.coffee +++ b/app/coffee/modules/userstories/detail.coffee @@ -42,15 +42,14 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$q", "$tgLocation", "$log", - "$appTitle", + "tgAppMetaService", "$tgNavUrls", "$tgAnalytics", - "$translate", - "tgLoader" + "$translate" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, - @log, @appTitle, @navUrls, @analytics, @translate, tgLoader) -> + @log, @appMetaService, @navUrls, @analytics, @translate) -> @scope.usRef = @params.usref @scope.sectionName = @translate.instant("US.SECTION_NAME") @.initializeEventHandlers() @@ -59,12 +58,32 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) # On Success promise.then => - @appTitle.set(@scope.us.subject + " - " + @scope.project.name) + @._setMeta() @.initializeOnDeleteGoToUrl() # On Error promise.then null, @.onInitialDataError.bind(@) - promise.finally tgLoader.pageLoaded + + _setMeta: -> + totalTasks = @scope.tasks.length + closedTasks = _.filter(@scope.tasks, (t) => @scope.taskStatusById[t.status].is_closed).length + progressPercentage = if totalTasks > 0 then Math.round(100 * closedTasks / totalTasks) else 0 + + title = @translate.instant("US.PAGE_TITLE", { + userStoryRef: "##{@scope.us.ref}" + userStorySubject: @scope.us.subject + projectName: @scope.project.name + }) + description = @translate.instant("US.PAGE_DESCRIPTION", { + userStoryStatus: @scope.statusById[@scope.us.status]?.name or "--" + userStoryPoints: @scope.us.total_points + userStoryDescription: angular.element(@scope.us.description_html or "").text() + userStoryClosedTasks: closedTasks + userStoryTotalTasks: totalTasks + userStoryProgressPercentage: progressPercentage + }) + + @appMetaService.setAll(title, description) initializeEventHandlers: -> @scope.$on "related-tasks:update", => @@ -73,16 +92,16 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin) @scope.$on "attachment:create", => @analytics.trackEvent("attachment", "create", "create attachment on userstory", 1) - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "attachment:edit", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "attachment:delete", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") @scope.$on "custom-attributes-values:edit", => - @rootscope.$broadcast("history:reload") + @rootscope.$broadcast("object:updated") initializeOnDeleteGoToUrl: -> ctx = {project: @scope.project.slug} @@ -299,6 +318,7 @@ UsStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $temp save = $qqueue.bindAdd (status) => us = $model.$modelValue.clone() + us.status = status $.fn.popover().closeAll() @@ -307,7 +327,7 @@ UsStatusButtonDirective = ($rootScope, $repo, $confirm, $loading, $qqueue, $temp onSuccess = -> $confirm.notify("success") - $rootScope.$broadcast("history:reload") + $rootScope.$broadcast("object:updated") $loading.finish($el.find(".level-name")) onError = -> @@ -389,7 +409,7 @@ UsTeamRequirementButtonDirective = ($rootscope, $tgrepo, $confirm, $loading, $qq promise = $tgrepo.save($model.$modelValue) promise.then => $loading.finish($el.find("label")) - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") promise.then null, -> $loading.finish($el.find("label")) @@ -451,7 +471,7 @@ UsClientRequirementButtonDirective = ($rootscope, $tgrepo, $confirm, $loading, $ promise = $tgrepo.save($model.$modelValue) promise.then => $loading.finish($el.find("label")) - $rootscope.$broadcast("history:reload") + $rootscope.$broadcast("object:updated") promise.then null, -> $loading.finish($el.find("label")) $confirm.notify("error") diff --git a/app/coffee/modules/wiki/main.coffee b/app/coffee/modules/wiki/main.coffee index 75968e1a..4386a1e0 100644 --- a/app/coffee/modules/wiki/main.coffee +++ b/app/coffee/modules/wiki/main.coffee @@ -46,15 +46,14 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgLocation", "$filter", "$log", - "$appTitle", + "tgAppMetaService", "$tgNavUrls", "$tgAnalytics", - "tgLoader", "$translate" ] constructor: (@scope, @rootscope, @repo, @model, @confirm, @rs, @params, @q, @location, - @filter, @log, @appTitle, @navUrls, @analytics, tgLoader, @translate) -> + @filter, @log, @appMetaService, @navUrls, @analytics, @translate) -> @scope.projectSlug = @params.pslug @scope.wikiSlug = @params.slug @scope.sectionName = "Wiki" @@ -62,12 +61,22 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) promise = @.loadInitialData() # On Success - promise.then () => - @appTitle.set("Wiki - " + @scope.project.name) + promise.then () => @._setMeta() # On Error promise.then null, @.onInitialDataError.bind(@) - promise.finally tgLoader.pageLoaded + + _setMeta: -> + title = @translate.instant("WIKI.PAGE_TITLE", { + wikiPageName: @scope.wiki.slug + projectName: unslugify(@scope.wiki.slug) + }) + description = @translate.instant("WIKI.PAGE_DESCRIPTION", { + wikiPageContent: angular.element(@scope.wiki.html or "").text() + totalEditions: @scope.wiki.editions or 0 + lastModifiedDate: moment(@scope.wiki.modified_date).format(@translate.instant("WIKI.DATETIME")) + }) + @appMetaService.setAll(title, description) loadProject: -> return @rs.projects.getBySlug(@params.pslug).then (project) => @@ -109,7 +118,8 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin) promise = @.loadProject() return promise.then (project) => @.fillUsersAndRoles(project.users, project.roles) - @q.all([@.loadWikiLinks(), @.loadWiki()]) + @q.all([@.loadWikiLinks(), @.loadWiki()]).then () => + delete: -> title = @translate.instant("WIKI.DELETE_LIGHTBOX_TITLE") diff --git a/app/coffee/utils.coffee b/app/coffee/utils.coffee index dc399ab8..621f09aa 100644 --- a/app/coffee/utils.coffee +++ b/app/coffee/utils.coffee @@ -126,6 +126,19 @@ startswith = (str1, str2) -> return _.str.startsWith(str1, str2) +truncate = (str, maxLength, suffix="...") -> + return str if (typeof str != "string") and not (str instanceof String) + + out = str.slice(0) + + if out.length > maxLength + out = out.substring(0, maxLength + 1) + out = out.substring(0, Math.min(out.length, out.lastIndexOf(" "))) + out = out + suffix + + return out + + sizeFormat = (input, precision=1) -> if isNaN(parseFloat(input)) or not isFinite(input) return "-" @@ -140,6 +153,37 @@ sizeFormat = (input, precision=1) -> size = (input / Math.pow(1024, number)).toFixed(precision) return "#{size} #{units[number]}" +stripTags = (str, exception) -> + if exception + pattern = new RegExp('<(?!' + exception + '\s*\/?)[^>]+>', 'gi') + return String(str).replace(pattern, '') + else + return String(str).replace(/<\/?[^>]+>/g, '') + +replaceTags = (str, tags, replace) -> + # open tag + pattern = new RegExp('<(' + tags + ')>', 'gi') + str = str.replace(pattern, '<' + replace + '>') + + # close tag + pattern = new RegExp('<\/(' + tags + ')>', 'gi') + str = str.replace(pattern, '') + + return str + +defineImmutableProperty = (obj, name, fn) => + Object.defineProperty obj, name, { + get: () => + if !_.isFunction(fn) + throw "defineImmutableProperty third param must be a function" + + fn_result = fn() + if fn_result && _.isObject(fn_result) + if fn_result.size == undefined + throw "defineImmutableProperty must return immutable data" + + return fn_result + } taiga = @.taiga taiga.nl2br = nl2br @@ -156,7 +200,11 @@ taiga.cancelTimeout = cancelTimeout taiga.scopeDefer = scopeDefer taiga.toString = toString taiga.joinStr = joinStr +taiga.truncate = truncate taiga.debounce = debounce taiga.debounceLeading = debounceLeading taiga.startswith = startswith taiga.sizeFormat = sizeFormat +taiga.stripTags = stripTags +taiga.replaceTags = replaceTags +taiga.defineImmutableProperty = defineImmutableProperty diff --git a/app/fonts/OpenSans-Light.eot b/app/fonts/OpenSans-Light.eot new file mode 100644 index 00000000..725db50b Binary files /dev/null and b/app/fonts/OpenSans-Light.eot differ diff --git a/app/fonts/OpenSans-Light.svg b/app/fonts/OpenSans-Light.svg new file mode 100644 index 00000000..b6daf106 --- /dev/null +++ b/app/fonts/OpenSans-Light.svg @@ -0,0 +1,581 @@ + + + + +Created by FontForge 20110222 at Thu May 12 12:49:24 2011 + By www-data +Digitized data copyright (c) 2010-2011, Google Corporation. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/fonts/OpenSans-Light.ttf b/app/fonts/OpenSans-Light.ttf new file mode 100644 index 00000000..3d29b04b Binary files /dev/null and b/app/fonts/OpenSans-Light.ttf differ diff --git a/app/fonts/taiga.eot b/app/fonts/taiga.eot index 39b73c70..f8cdfc90 100644 Binary files a/app/fonts/taiga.eot and b/app/fonts/taiga.eot differ diff --git a/app/fonts/taiga.svg b/app/fonts/taiga.svg index 018e647f..435c7a92 100644 --- a/app/fonts/taiga.svg +++ b/app/fonts/taiga.svg @@ -30,16 +30,13 @@ - - - @@ -49,7 +46,6 @@ - @@ -59,4 +55,9 @@ + + + + + diff --git a/app/fonts/taiga.ttf b/app/fonts/taiga.ttf index 6b27df58..67c906cc 100644 Binary files a/app/fonts/taiga.ttf and b/app/fonts/taiga.ttf differ diff --git a/app/fonts/taiga.woff b/app/fonts/taiga.woff index 48f74d8f..5c346f3a 100644 Binary files a/app/fonts/taiga.woff and b/app/fonts/taiga.woff differ diff --git a/app/images/github-help.png b/app/images/github-help.png deleted file mode 100644 index 795fd9f5..00000000 Binary files a/app/images/github-help.png and /dev/null differ diff --git a/app/images/logo-color.png b/app/images/logo-color.png new file mode 100644 index 00000000..a613d40f Binary files /dev/null and b/app/images/logo-color.png differ diff --git a/app/images/menu-vert.png b/app/images/menu-vert.png new file mode 100644 index 00000000..d1a8e7c2 Binary files /dev/null and b/app/images/menu-vert.png differ diff --git a/app/images/quote.png b/app/images/quote.png new file mode 100644 index 00000000..884fdd8e Binary files /dev/null and b/app/images/quote.png differ diff --git a/app/index.jade b/app/index.jade index e1ff61b8..105ca868 100644 --- a/app/index.jade +++ b/app/index.jade @@ -2,22 +2,23 @@ doctype html html(lang="en") head meta(charset="utf-8") - title Taiga meta(http-equiv="content-type", content="text/html; charset=utf-8") - meta(name="description", content="Taiga Landing page") - meta(name="keywords", content="Agile, Taiga, Management, Github") + meta(name="fragment", content="!") + + // Main meta + title Taiga + meta(name="description", content="Taiga is a project management platform for startups and agile developers & designers who want a simple, beautiful tool that makes work truly enjoyable.") + meta(name="keywords", content="agile, scrum, taiga, management, project, developer, designer, user experience") //-meta(name="viewport", content="width=device-width, user-scalable=no") link(rel="stylesheet", href="/styles/main.css") link(rel="icon", type="image/png", href="/images/favicon.png") + //- PRERENDER SERVICE: This is to know when the page is completely loaded. + script(type='text/javascript'). + window.prerenderReady = false; body(tg-main) - include partials/includes/modules/projects-nav - - //- the content of nav.menu is in coffe.modules.base TaigaMain directive - nav.menu.hidden(tg-project-menu) - - include partials/includes/components/notification-message + div(tg-navigation-bar) div.master(ng-view) @@ -31,13 +32,11 @@ html(lang="en") include partials/includes/modules/lightbox-generic-error div.lightbox.lightbox-generic-loading include partials/includes/modules/lightbox-generic-loading - div.lightbox.lightbox-search(tg-search-box) - include partials/includes/modules/lightbox-search - div.lightbox.lightbox-feedback.lightbox-generic-form(tg-lb-feedback) - include partials/includes/modules/lightbox-feedback include partials/includes/modules/loader + include partials/includes/components/notification-message + script(src="/js/libs.js?v=#{v}") script(src="/js/templates.js?v=#{v}") script(src="/js/app-loader.js?v=#{v}") diff --git a/app/js/jquery.ui.git-custom.js b/app/js/jquery.ui.git-custom.js index 327fb2a1..855907fe 100644 --- a/app/js/jquery.ui.git-custom.js +++ b/app/js/jquery.ui.git-custom.js @@ -1,24 +1,36 @@ -/*! jQuery UI - v1.11.1-pre - 2014-07-24 +/*! jQuery UI - v1.11.4 - 2015-05-19 * http://jqueryui.com -* Includes: core.js, widget.js, mouse.js, draggable.js, droppable.js, resizable.js, selectable.js, sortable.js, effect.js, accordion.js, autocomplete.js, button.js, datepicker.js, dialog.js, effect-blind.js, effect-bounce.js, effect-clip.js, effect-drop.js, effect-explode.js, effect-fade.js, effect-fold.js, effect-highlight.js, effect-puff.js, effect-pulsate.js, effect-scale.js, effect-shake.js, effect-size.js, effect-slide.js, effect-transfer.js, menu.js, position.js, progressbar.js, selectmenu.js, slider.js, spinner.js, tabs.js, tooltip.js -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ +* Includes: core.js, widget.js, mouse.js, position.js, draggable.js, droppable.js, resizable.js, selectable.js, sortable.js, autocomplete.js, menu.js +* Copyright 2015 jQuery Foundation and other contributors; Licensed MIT */ + (function( factory ) { if ( typeof define === "function" && define.amd ) { // AMD. Register as an anonymous module. - define( [ "jquery" ], factory ); + define([ "jquery" ], factory ); } else { // Browser globals factory( jQuery ); } }(function( $ ) { +/*! + * jQuery UI Core 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/ui-core/ + */ + // $.ui might exist from components with no dependencies, e.g., $.ui.position $.ui = $.ui || {}; $.extend( $.ui, { - version: "@VERSION", + version: "1.11.4", keyCode: { BACKSPACE: 8, @@ -42,15 +54,16 @@ $.extend( $.ui, { // plugins $.fn.extend({ - scrollParent: function() { + scrollParent: function( includeHidden ) { var position = this.css( "position" ), excludeStaticParent = position === "absolute", + overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, scrollParent = this.parents().filter( function() { var parent = $( this ); if ( excludeStaticParent && parent.css( "position" ) === "static" ) { return false; } - return (/(auto|scroll)/).test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + parent.css( "overflow-x" ) ); + return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + parent.css( "overflow-x" ) ); }).eq( 0 ); return position === "fixed" || !scrollParent.length ? $( this[ 0 ].ownerDocument || document ) : scrollParent; @@ -87,10 +100,10 @@ function focusable( element, isTabIndexNotNaN ) { if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { return false; } - img = $( "img[usemap=#" + mapName + "]" )[0]; + img = $( "img[usemap='#" + mapName + "']" )[ 0 ]; return !!img && visible( img ); } - return ( /input|select|textarea|button|object/.test( nodeName ) ? + return ( /^(input|select|textarea|button|object)$/.test( nodeName ) ? !element.disabled : "a" === nodeName ? element.href || isTabIndexNotNaN : @@ -294,19 +307,18 @@ $.ui.plugin = { } }; -})); -(function( factory ) { - if ( typeof define === "function" && define.amd ) { +/*! + * jQuery UI Widget 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/jQuery.widget/ + */ - // AMD. Register as an anonymous module. - define( [ "jquery" ], factory ); - } else { - - // Browser globals - factory( jQuery ); - } -}(function( $ ) { var widget_uuid = 0, widget_slice = Array.prototype.slice; @@ -324,7 +336,7 @@ $.cleanData = (function( orig ) { } // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} + } catch ( e ) {} } orig( elems ); }; @@ -478,11 +490,6 @@ $.widget.bridge = function( name, object ) { args = widget_slice.call( arguments, 1 ), returnValue = this; - // allow multiple hashes to be passed on init - options = !isMethodCall && args.length ? - $.widget.extend.apply( null, [ options ].concat(args) ) : - options; - if ( isMethodCall ) { this.each(function() { var methodValue, @@ -507,6 +514,12 @@ $.widget.bridge = function( name, object ) { } }); } else { + + // Allow multiple hashes to be passed on init + if ( args.length ) { + options = $.widget.extend.apply( null, [ options ].concat(args) ); + } + this.each(function() { var instance = $.data( this, fullName ); if ( instance ) { @@ -542,10 +555,6 @@ $.Widget.prototype = { this.element = $( element ); this.uuid = widget_uuid++; this.eventNamespace = "." + this.widgetName + this.uuid; - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); this.bindings = $(); this.hoverable = $(); @@ -568,6 +577,11 @@ $.Widget.prototype = { this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); } + this.options = $.widget.extend( {}, + this.options, + this._getCreateOptions(), + options ); + this._create(); this._trigger( "create", null, this._getCreateEventData() ); this._init(); @@ -730,8 +744,14 @@ $.Widget.prototype = { }, _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; + eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + + this.eventNamespace; element.unbind( eventName ).undelegate( eventName ); + + // Clear the stack to avoid memory leaks (#10056) + this.bindings = $( this.bindings.not( element ).get() ); + this.focusable = $( this.focusable.not( element ).get() ); + this.hoverable = $( this.hoverable.not( element ).get() ); }, _delay: function( handler, delay ) { @@ -789,6 +809,7 @@ $.Widget.prototype = { } } } + this.element.trigger( event, data ); return !( $.isFunction( callback ) && callback.apply( this.element[0], [ event ].concat( data ) ) === false || @@ -832,32 +853,28 @@ $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { }; }); -return $.widget; +var widget = $.widget; -})); -(function( factory ) { - if ( typeof define === "function" && define.amd ) { +/*! + * jQuery UI Mouse 1.11.4 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/mouse/ + */ - // AMD. Register as an anonymous module. - define([ - "jquery", - "./widget" - ], factory ); - } else { - - // Browser globals - factory( jQuery ); - } -}(function( $ ) { var mouseHandled = false; $( document ).mouseup( function() { mouseHandled = false; }); -return $.widget("ui.mouse", { - version: "@VERSION", +var mouse = $.widget("ui.mouse", { + version: "1.11.4", options: { cancel: "input,textarea,button,select,option", distance: 1, @@ -898,6 +915,8 @@ return $.widget("ui.mouse", { return; } + this._mouseMoved = false; + // we may have missed mouseup (out of window) (this._mouseStarted && this._mouseUp(event)); @@ -951,13 +970,23 @@ return $.widget("ui.mouse", { }, _mouseMove: function(event) { - // IE mouseup check - mouseup happened when mouse was out of window - if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { - return this._mouseUp(event); + // Only check for mouseups outside the document if you've moved inside the document + // at least once. This prevents the firing of mouseup in the case of IE<9, which will + // fire a mousemove event if content is placed under the cursor. See #7778 + // Support: IE <9 + if ( this._mouseMoved ) { + // IE mouseup check - mouseup happened when mouse was out of window + if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { + return this._mouseUp(event); - // Iframe mouseup check - mouseup occurred in another document - } else if ( !event.which ) { - return this._mouseUp( event ); + // Iframe mouseup check - mouseup occurred in another document + } else if ( !event.which ) { + return this._mouseUp( event ); + } + } + + if ( event.which || event.button ) { + this._mouseMoved = true; } if (this._mouseStarted) { @@ -1012,11685 +1041,18 @@ return $.widget("ui.mouse", { _mouseCapture: function(/* event */) { return true; } }); -})); - -(function( factory ) { - if ( typeof define === "function" && define.amd ) { - - // AMD. Register as an anonymous module. - define([ - "jquery", - "./core", - "./mouse", - "./widget" - ], factory ); - } else { - - // Browser globals - factory( jQuery ); - } -}(function( $ ) { - -$.widget("ui.draggable", $.ui.mouse, { - version: "@VERSION", - widgetEventPrefix: "drag", - options: { - addClasses: true, - appendTo: "parent", - axis: false, - connectToSortable: false, - containment: false, - cursor: "auto", - cursorAt: false, - grid: false, - handle: false, - helper: "original", - iframeFix: false, - opacity: false, - refreshPositions: false, - revert: false, - revertDuration: 500, - scope: "default", - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - snap: false, - snapMode: "both", - snapTolerance: 20, - stack: false, - zIndex: false, - - // callbacks - drag: null, - start: null, - stop: null - }, - _create: function() { - - if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { - this.element[0].style.position = "relative"; - } - if (this.options.addClasses){ - this.element.addClass("ui-draggable"); - } - if (this.options.disabled){ - this.element.addClass("ui-draggable-disabled"); - } - this._setHandleClassName(); - - this._mouseInit(); - }, - - _setOption: function( key, value ) { - this._super( key, value ); - if ( key === "handle" ) { - this._removeHandleClassName(); - this._setHandleClassName(); - } - }, - - _destroy: function() { - if ( ( this.helper || this.element ).is( ".ui-draggable-dragging" ) ) { - this.destroyOnClear = true; - return; - } - this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); - this._removeHandleClassName(); - this._mouseDestroy(); - }, - - _mouseCapture: function(event) { - - var document = this.document[ 0 ], - o = this.options; - - // support: IE9 - // IE9 throws an "Unspecified error" accessing document.activeElement from an