diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65ffd3ec..d9a20a59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
- Activity timeline
- Global user configuration
- User contacts
+- Improve SEO, fix meta tags and added social meta tags
### Misc
- Improve performance: remove some unnecessary calls to the api.
diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee
index 5cdb7edd..c6f2693a 100644
--- a/app/coffee/app.coffee
+++ b/app/coffee/app.coffee
@@ -28,21 +28,26 @@ 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, $compileProvider, $translateProvider) ->
+
+configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEventsProvider,
+ $compileProvider, $translateProvider) ->
$routeProvider.when("/",
{
templateUrl: "home/home.html",
access: {
requiresLogin: true
},
- title: "PROJECT.WELCOME",
+ title: "HOME.PAGE_TITLE",
+ description: "HOME.PAGE_DESCRIPTION",
loader: true
}
)
@@ -53,7 +58,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
access: {
requiresLogin: true
},
- title: "PROJECT.SECTION_PROJECTS",
+ title: "PROJECTS.PAGE_TITLE",
+ description: "PROJECTS.PAGE_DESCRIPTION",
loader: true,
controller: "ProjectsListing",
controllerAs: "vm"
@@ -305,17 +311,47 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven
# 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",
@@ -482,7 +518,7 @@ i18nInit = (lang, $translate) ->
checksley.updateMessages('default', messages)
-init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, $appTitle, projectService, loaderService) ->
+init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $navUrls, appMetaService, projectService, loaderService) ->
$log.debug("Initialize application")
# Taiga Plugins
@@ -517,8 +553,11 @@ init = ($log, $rootscope, $auth, $events, $analytics, $translate, $location, $na
else
projectService.cleanProject()
- if next.title
- $translate(next.title).then (text) => $appTitle.set(text)
+ if next.title or next.description
+ title = next.title or ""
+ description = next.description or ""
+ $translate([title, description]).then (translations) =>
+ appMetaService.setAll(translations[title], translations[description])
if next.loader
loaderService.startWithAutoClose()
@@ -592,7 +631,7 @@ module.run([
"$translate",
"$tgLocation",
"$tgNavUrls",
- "$appTitle",
+ "tgAppMetaService",
"tgProjectService",
"tgLoader",
init
diff --git a/app/coffee/modules/admin/memberships.coffee b/app/coffee/modules/admin/memberships.coffee
index 1d5953d4..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(@)
@@ -378,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")
@@ -415,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 ada135de..b3c117ae 100644
--- a/app/coffee/modules/admin/project-profile.coffee
+++ b/app/coffee/modules/admin/project-profile.coffee
@@ -47,28 +47,31 @@ 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.getBySlug(@params.pslug).then (project) =>
@@ -119,7 +122,9 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, proje
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)
@@ -137,7 +142,9 @@ ProjectProfileDirective = ($repo, $confirm, $loading, $navurls, $location, proje
return {link:link}
-module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", "$tgNavUrls", "$tgLocation", "tgProjectService", ProjectProfileDirective])
+module.directive("tgProjectProfile", ["$tgRepo", "$tgConfirm", "$tgLoading", "$tgNavUrls", "$tgLocation",
+ "tgProjectService", ProjectProfileDirective])
+
#############################################################################
## Project Default Values Directive
@@ -224,7 +231,8 @@ ProjectModulesDirective = ($repo, $confirm, $loading, projectService) ->
return {link:link}
-module.directive("tgProjectModules", ["$tgRepo", "$tgConfirm", "$tgLoading", "tgProjectService", 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 ee520752..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,12 +59,12 @@ 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(@)
@@ -383,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
@@ -640,4 +650,5 @@ ProjectCustomAttributesDirective = ($log, $confirm, animationFrame, $translate)
return {link: link}
-module.directive("tgProjectCustomAttributes", ["$log", "$tgConfirm", "animationFrame", "$translate", 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 f466c90d..b41cd2e5 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,8 +59,9 @@ 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(@)
diff --git a/app/coffee/modules/admin/third-parties.coffee b/app/coffee/modules/admin/third-parties.coffee
index b05318c3..737a8144 100644
--- a/app/coffee/modules/admin/third-parties.coffee
+++ b/app/coffee/modules/admin/third-parties.coffee
@@ -41,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"
@@ -54,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(@)
@@ -292,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")
@@ -305,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(@)
@@ -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(@)
@@ -388,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")
@@ -400,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(@)
diff --git a/app/coffee/modules/backlog/main.coffee b/app/coffee/modules/backlog/main.coffee
index 9960c3e8..845f33a7 100644
--- a/app/coffee/modules/backlog/main.coffee
+++ b/app/coffee/modules/backlog/main.coffee
@@ -45,7 +45,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
"$routeParams",
"$q",
"$tgLocation",
- "$appTitle",
+ "tgAppMetaService",
"$tgNavUrls",
"$tgEvents",
"$tgAnalytics",
@@ -53,7 +53,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q,
- @location, @appTitle, @navUrls, @events, @analytics, @translate) ->
+ @location, @appMetaService, @navUrls, @events, @analytics, @translate) ->
bindMethods(@)
@scope.sectionName = @translate.instant("BACKLOG.SECTION_NAME")
@@ -66,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
diff --git a/app/coffee/modules/base/contrib.coffee b/app/coffee/modules/base/contrib.coffee
index a5e3d695..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,9 +39,6 @@ class ContribController extends taiga.Controller
promise = @.loadInitialData()
- promise.then () =>
- @appTitle.set(@scope.project.name)
-
promise.then null, =>
@confirm.notify("error")
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/issues/detail.coffee b/app/coffee/modules/issues/detail.coffee
index c85b6a65..36bb542e 100644
--- a/app/coffee/modules/issues/detail.coffee
+++ b/app/coffee/modules/issues/detail.coffee
@@ -44,14 +44,14 @@ class IssueDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$q",
"$tgLocation",
"$log",
- "$appTitle",
+ "tgAppMetaService",
"$tgAnalytics",
"$tgNavUrls",
"$translate"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
- @log, @appTitle, @analytics, @navUrls, @translate) ->
+ @log, @appMetaService, @analytics, @navUrls, @translate) ->
@scope.issueRef = @params.issueref
@scope.sectionName = @translate.instant("ISSUES.SECTION_NAME")
@.initializeEventHandlers()
@@ -60,12 +60,27 @@ 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(@)
+ _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("object:updated")
diff --git a/app/coffee/modules/issues/list.coffee b/app/coffee/modules/issues/list.coffee
index 9779dc1e..d1ef31f0 100644
--- a/app/coffee/modules/issues/list.coffee
+++ b/app/coffee/modules/issues/list.coffee
@@ -47,14 +47,15 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
"$routeParams",
"$q",
"$tgLocation",
- "$appTitle",
+ "tgAppMetaService",
"$tgNavUrls",
"$tgEvents",
- "$tgAnalytics"
+ "$tgAnalytics",
+ "$translate"
]
- constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appTitle,
- @navUrls, @events, @analytics) ->
+ constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appMetaService,
+ @navUrls, @events, @analytics, @translate) ->
@scope.sectionName = "Issues"
@scope.filters = {}
@@ -69,7 +70,12 @@ 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(@)
diff --git a/app/coffee/modules/kanban/main.coffee b/app/coffee/modules/kanban/main.coffee
index 8edc40a3..137521c6 100644
--- a/app/coffee/modules/kanban/main.coffee
+++ b/app/coffee/modules/kanban/main.coffee
@@ -58,7 +58,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
"$routeParams",
"$q",
"$tgLocation",
- "$appTitle",
+ "tgAppMetaService",
"$tgNavUrls",
"$tgEvents",
"$tgAnalytics",
@@ -66,7 +66,7 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
- @appTitle, @navUrls, @events, @analytics, @translate) ->
+ @appMetaService, @navUrls, @events, @analytics, @translate) ->
bindMethods(@)
@@ -78,7 +78,12 @@ 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(@)
diff --git a/app/coffee/modules/search.coffee b/app/coffee/modules/search.coffee
index 30955c32..91a974a5 100644
--- a/app/coffee/modules/search.coffee
+++ b/app/coffee/modules/search.coffee
@@ -43,17 +43,23 @@ class SearchController extends mixOf(taiga.Controller, taiga.PageMixin)
"$routeParams",
"$q",
"$tgLocation",
- "$appTitle",
- "$tgNavUrls"
+ "tgAppMetaService",
+ "$tgNavUrls",
+ "$translate"
]
- constructor: (@scope, @repo, @rs, @params, @q, @location, @appTitle, @navUrls) ->
+ 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(@)
diff --git a/app/coffee/modules/taskboard/main.coffee b/app/coffee/modules/taskboard/main.coffee
index c9aae4af..f4e82d92 100644
--- a/app/coffee/modules/taskboard/main.coffee
+++ b/app/coffee/modules/taskboard/main.coffee
@@ -44,7 +44,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
"$tgResources",
"$routeParams",
"$q",
- "$appTitle",
+ "tgAppMetaService",
"$tgLocation",
"$tgNavUrls"
"$tgEvents"
@@ -52,7 +52,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
"$translate"
]
- constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appTitle, @location, @navUrls,
+ constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appMetaService, @location, @navUrls,
@events, @analytics, @translate) ->
bindMethods(@)
@@ -62,12 +62,31 @@ 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(@)
+ _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
# a big overhead. It should be optimized in near future.
@@ -255,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 5e1c790c..228c4aff 100644
--- a/app/coffee/modules/tasks/detail.coffee
+++ b/app/coffee/modules/tasks/detail.coffee
@@ -42,14 +42,14 @@ class TaskDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$q",
"$tgLocation",
"$log",
- "$appTitle",
+ "tgAppMetaService",
"$tgNavUrls",
"$tgAnalytics",
"$translate"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
- @log, @appTitle, @navUrls, @analytics, @translate) ->
+ @log, @appMetaService, @navUrls, @analytics, @translate) ->
@scope.taskRef = @params.taskref
@scope.sectionName = @translate.instant("TASK.SECTION_NAME")
@.initializeEventHandlers()
@@ -57,11 +57,23 @@ 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(@)
+ _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)
diff --git a/app/coffee/modules/team/main.coffee b/app/coffee/modules/team/main.coffee
index 5991b7c1..355a030e 100644
--- a/app/coffee/modules/team/main.coffee
+++ b/app/coffee/modules/team/main.coffee
@@ -39,21 +39,26 @@ class TeamController extends mixOf(taiga.Controller, taiga.PageMixin)
"$q",
"$location",
"$tgNavUrls",
- "$appTitle",
+ "tgAppMetaService",
"$tgAuth",
"$translate",
"tgProjectService"
]
- constructor: (@scope, @rootscope, @repo, @rs, @params, @q, @location, @navUrls, @appTitle, @auth, @translate, @projectService) ->
+ 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(@)
diff --git a/app/coffee/modules/userstories/detail.coffee b/app/coffee/modules/userstories/detail.coffee
index 1d8acd13..006da05c 100644
--- a/app/coffee/modules/userstories/detail.coffee
+++ b/app/coffee/modules/userstories/detail.coffee
@@ -42,14 +42,14 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$q",
"$tgLocation",
"$log",
- "$appTitle",
+ "tgAppMetaService",
"$tgNavUrls",
"$tgAnalytics",
"$translate"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
- @log, @appTitle, @navUrls, @analytics, @translate) ->
+ @log, @appMetaService, @navUrls, @analytics, @translate) ->
@scope.usRef = @params.usref
@scope.sectionName = @translate.instant("US.SECTION_NAME")
@.initializeEventHandlers()
@@ -58,12 +58,33 @@ 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(@)
+ _setMeta: ->
+ totalTasks = @scope.tasks.length
+ closedTasks = _.filter(@scope.tasks, (t) => @scope.taskStatusById[t.status].is_closed).length
+ progressPercentage = if totalTasks > 0 then 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", =>
@.loadUs()
diff --git a/app/coffee/modules/wiki/main.coffee b/app/coffee/modules/wiki/main.coffee
index 01f8fc9f..4386a1e0 100644
--- a/app/coffee/modules/wiki/main.coffee
+++ b/app/coffee/modules/wiki/main.coffee
@@ -46,14 +46,14 @@ class WikiDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
"$tgLocation",
"$filter",
"$log",
- "$appTitle",
+ "tgAppMetaService",
"$tgNavUrls",
"$tgAnalytics",
"$translate"
]
constructor: (@scope, @rootscope, @repo, @model, @confirm, @rs, @params, @q, @location,
- @filter, @log, @appTitle, @navUrls, @analytics, @translate) ->
+ @filter, @log, @appMetaService, @navUrls, @analytics, @translate) ->
@scope.projectSlug = @params.pslug
@scope.wikiSlug = @params.slug
@scope.sectionName = "Wiki"
@@ -61,12 +61,23 @@ 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(@)
+ _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) =>
if not project.is_wiki_activated
diff --git a/app/coffee/utils.coffee b/app/coffee/utils.coffee
index 58874d4f..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 "-"
@@ -187,6 +200,7 @@ taiga.cancelTimeout = cancelTimeout
taiga.scopeDefer = scopeDefer
taiga.toString = toString
taiga.joinStr = joinStr
+taiga.truncate = truncate
taiga.debounce = debounce
taiga.debounceLeading = debounceLeading
taiga.startswith = startswith
diff --git a/app/index.jade b/app/index.jade
index c8068c33..105ca868 100644
--- a/app/index.jade
+++ b/app/index.jade
@@ -3,10 +3,12 @@ html(lang="en")
head
meta(charset="utf-8")
meta(http-equiv="content-type", content="text/html; charset=utf-8")
+ 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="fragment", content="!")
//-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")
diff --git a/app/locales/locale-en.json b/app/locales/locale-en.json
index e389d273..38459a8e 100644
--- a/app/locales/locale-en.json
+++ b/app/locales/locale-en.json
@@ -236,20 +236,105 @@
"ADD_WIKI_LINKS": "Add wiki links",
"DELETE_WIKI_LINKS": "Delete wiki links"
}
+ },
+ "META": {
+ "PAGE_TITLE": "Taiga",
+ "PAGE_DESCRIPTION": "Taiga is a project management platform for startups and agile developers & designers who want a simple, beautiful tool that makes work truly enjoyable."
}
},
+ "LOGIN": {
+ "PAGE_TITLE": "Login - Taiga",
+ "PAGE_DESCRIPTION": "Logging in to Taiga, a project management platform for startups and agile developers & designers who want a simple, beautif ul tool that makes work truly enjoyable."
+ },
"AUTH": {
"INVITED_YOU": "has invited you to join the project",
"NOT_REGISTERED_YET": "Not registered yet?",
"REGISTER": "Register",
"CREATE_ACCOUNT": "create your free account here"
},
+ "LOGIN_COMMON": {
+ "HEADER": "I already have a Taiga login",
+ "PLACEHOLDER_AUTH_NAME": "Username or email (case sensitive)",
+ "LINK_FORGOT_PASSWORD": "Forgot it?",
+ "TITLE_LINK_FORGOT_PASSWORD": "Did you forgot your password?",
+ "ACTION_ENTER": "Enter",
+ "ACTION_SIGN_IN": "Sign in",
+ "PLACEHOLDER_AUTH_PASSWORD": "Password (case sensitive)"
+ },
+ "LOGIN_FORM": {
+ "ERROR_AUTH_INCORRECT": "According to our Oompa Loompas, your username/email or password are incorrect.",
+ "ERROR_GENERIC": "According to our Oompa Loompas there was an error.",
+ "SUCCESS": "Our Oompa Loompas are happy, welcome to Taiga."
+ },
+ "REGISTER": {
+ "PAGE_TITLE": "Register - Taiga",
+ "PAGE_DESCRIPTION": "Create your account in Taiga, a project management platform for startups and agile developers & designers who want a simple, beautif ul tool that makes work truly enjoyable."
+ },
+ "REGISTER_FORM": {
+ "TITLE": "Register a new Taiga account (free)",
+ "PLACEHOLDER_NAME": "Pick a username (case sensitive)",
+ "PLACEHOLDER_FULL_NAME": "Pick your full name",
+ "PLACEHOLDER_EMAIL": "Your email",
+ "PLACEHOLDER_PASSWORD": "Set a password (case sensitive)",
+ "ACTION_SIGN_UP": "Sign up",
+ "TITLE_LINK_LOGIN": "Log in",
+ "LINK_LOGIN": "Are you already registered? Log in"
+ },
+ "FORGOT_PASSWORD": {
+ "PAGE_TITLE": "Forgot password - Taiga",
+ "PAGE_DESCRIPTION": "Enter your username or email to get a new password and you can access to Taiga again."
+ },
+ "FORGOT_PASSWORD_FORM": {
+ "TITLE": "Oops, did you forget your password?",
+ "SUBTITLE": "Enter your username or email to get a new one",
+ "PLACEHOLDER_FIELD": "Username or email",
+ "ACTION_RESET_PASSWORD": "Reset Password",
+ "LINK_CANCEL": "Nah, take me back. I think I remember it.",
+ "SUCCESS": "Check your inbox!
We have sent you an email with the instructions to set a new password",
+ "ERROR": "According to our Oompa Loompas, your are not registered yet."
+ },
+ "CHANGE_PASSWORD": {
+ "PAGE_TITLE": "Change you password - Taiga",
+ "PAGE_DESCRIPTION": "Set a new passoword for your Taiga account and hey!, you may want to eat some more iron-rich food, it's good for your brain :P",
+ "SECTION_NAME": "Change password",
+ "FIELD_CURRENT_PASSWORD": "Current password",
+ "PLACEHOLDER_CURRENT_PASSWORD": "Your current password (or empty if you have no password yet)",
+ "FIELD_NEW_PASSWORD": "New password",
+ "PLACEHOLDER_NEW_PASSWORD": "Type a new password",
+ "FIELD_RETYPE_PASSWORD": "Retype new password",
+ "PLACEHOLDER_RETYPE_PASSWORD": "Retype the new password",
+ "ERROR_PASSWORD_MATCH": "The passwords doesn't match"
+ },
+ "CHANGE_PASSWORD_RECOVERY_FORM": {
+ "TITLE": "Create a new Taiga pass",
+ "SUBTITLE": "And hey, you may want to eat some more iron-rich food, it's good for your brain :P",
+ "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Recover password token",
+ "LINK_NEED_TOKEN": "Need one?",
+ "TITLE_LINK_NEED_TOKEN": "Did you need a token to recover your password because you forgot it?",
+ "PLACEHOLDER_NEW_PASSWORD": "New password",
+ "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Re-type new password",
+ "ACTION_RESET_PASSWORD": "Reset Password",
+ "SUCCESS": "Our Oompa Loompas saved your new password.
Try to sign in with it."
+ },
+ "INVITATION": {
+ "PAGE_TITLE": "Invitation acceptance - Taiga",
+ "PAGE_DESCRIPTION": "Accept the invitation to join a project in Taiga, a project management platform for startups and agile developers & designers who want a simple, beautif ul tool that makes work truly enjoyable."
+ },
+ "INVITATION_LOGIN_FORM": {
+ "NOT_FOUND": "Ooops, we have a problem
Our Oompa Loompas can't find your invitation.",
+ "SUCCESS": "You've successfully joined this project, Welcome to {{project_name}}",
+ "ERROR": "According to our Oompa Loompas, your are not registered yet or typed an invalid password."
+ },
"HOME": {
+ "PAGE_TITLE": "Home - Taiga",
+ "PAGE_DESCRIPTION": "The Taiga home page with your main projects and all your assigned and watched user stories, tasks and issues",
"EMPTY_WATCHING": "Follow the projects, User Stories, Tasks, Issues... that you want to know about :)",
"WORKING_ON_SECTION": "Working on",
"WATCHING_SECTION": "Watching"
},
"PROJECTS": {
+ "PAGE_TITLE": "My projects - Taiga",
+ "PAGE_DESCRIPTION": "A list with all your projects, you can reorder or create a new one.",
"MY_PROJECTS": "My projects"
},
"ATTACHMENT": {
@@ -289,6 +374,7 @@
},
"MEMBERSHIPS": {
"TITLE": "Manage members",
+ "PAGE_TITLE": "Memberships - {{projectName}}",
"ADD_BUTTON": "+ New member",
"ADD_BUTTON_TITLE": "Add new member"
},
@@ -324,7 +410,7 @@
"SALT_CHAT_ROOM": "If you want you can append a salt code to the name of the chat room"
},
"PROJECT_PROFILE": {
- "PAGE_TITLE": "Project profile - {{sectionName}} - {{projectName}}",
+ "PAGE_TITLE": "{{sectionName}} - Project profile - {{projectName}}",
"PROJECT_DETAILS": "Project details",
"PROJECT_NAME": "Project name",
"PROJECT_SLUG": "Project slug",
@@ -365,23 +451,26 @@
"ISSUE_ADD": "Add a custom field in issues"
},
"PROJECT_VALUES": {
- "APP_TITLE": "Project values - {{sectionName}} - {{projectName}}",
+ "PAGE_TITLE": "{{sectionName}} - Project values - {{projectName}}",
"REPLACEMENT": "All items with this value will be changed to",
"ERROR_DELETE_ALL": "You can't delete all values."
},
"PROJECT_VALUES_POINTS": {
- "TITLE": "Us points",
+ "TITLE": "Points",
"SUBTITLE": "Specify the points your user stories could be estimated to",
+ "US_TITLE": "US points",
"ACTION_ADD": "Add new point"
},
"PROJECT_VALUES_PRIORITIES": {
- "TITLE": "Issue priorities",
+ "TITLE": "Priorities",
"SUBTITLE": "Specify the priorities your issues will have",
+ "ISSUE_TITLE": "Issue priorities",
"ACTION_ADD": "Add new priority"
},
"PROJECT_VALUES_SEVERITIES": {
- "TITLE": "Issue severities",
+ "TITLE": "Severities",
"SUBTITLE": "Specify the severities your issues will have",
+ "ISSUE_TITLE": "Issue severities",
"ACTION_ADD": "Add new severity"
},
"PROJECT_VALUES_STATUS": {
@@ -398,7 +487,7 @@
"ACTION_ADD": "Add new {{objName}}"
},
"ROLES": {
- "SECTION_NAME": "Roles - {{projectName}}",
+ "PAGE_TITLE": "Roles - {{projectName}}",
"WARNING_NO_ROLE": "Be careful, no role in your project will be able to estimate the point value for user stories",
"HELP_ROLE_ENABLED": "When enabled, members assigned to this role will be able to estimate the point value for user stories",
"COUNT_MEMBERS": "{{ role.members_count }} members with this role",
@@ -415,20 +504,20 @@
},
"BITBUCKET": {
"SECTION_NAME": "Bitbucket",
- "APP_TITLE": "Bitbucket - {{projectName}}",
+ "PAGE_TITLE": "Bitbucket - {{projectName}}",
"INFO_VERIFYING_IP": "Bitbucket requests are not signed so the best way of verifying the origin is by IP. If the field is empty there will be no IP validation."
},
"GITLAB": {
"SECTION_NAME": "Gitlab",
- "APP_TITLE": "Gitlab - {{projectName}}",
+ "PAGE_TITLE": "Gitlab - {{projectName}}",
"INFO_VERIFYING_IP": "Gitlab requests are not signed so the best way of verifying the origin is by IP. If the field is empty there will be no IP validation."
},
"GITHUB": {
"SECTION_NAME": "Github",
- "APP_TITLE": "Github - {{projectName}}"
+ "PAGE_TITLE": "Github - {{projectName}}"
},
"WEBHOOKS": {
- "APP_TITLE": "Webhooks - {{projectName}}",
+ "PAGE_TITLE": "Webhooks - {{projectName}}",
"SECTION_NAME": "Webhooks",
"SUBTITLE": "Webhooks notify external services about events in Taiga, like comments, user stories....",
"ADD_NEW": "Add a New Webhook",
@@ -454,6 +543,7 @@
"WEBHOOK_NAME": "Webhook '{{name}}'"
},
"CUSTOM_ATTRIBUTES": {
+ "PAGE_TITLE": "{{sectionName}} - Custom Attributes - {{projectName}}",
"ADD": "Add custom field",
"EDIT": "Edit Custom Field",
"DELETE": "Delete Custom Field",
@@ -533,6 +623,7 @@
},
"USER": {
"PROFILE": {
+ "PAGE_TITLE": "{{userFullName}} (@{{userUsername}})",
"EDIT": "Edit profile",
"FOLLOW": "Follow",
"PROJECTS": "Projects",
@@ -555,6 +646,7 @@
}
},
"PROJECT": {
+ "PAGE_TITLE": "{{projectName}}",
"WELCOME": "Welcome",
"SECTION_PROJECTS": "Projects",
"HELP": "Reorder your projects to set in the top the most used ones.
The top 10 projects will appear in the top navigation bar project list",
@@ -685,6 +777,9 @@
}
},
"US": {
+ "PAGE_TITLE": "{{userStorySubject}} - User Story {{userStoryRef}} - {{projectName}}",
+ "PAGE_DESCRIPTION": "Status: {{userStoryStatus }}. Completed {{userStoryProgressPercentage}}% ({{userStoryClosedTasks}} of {{userStoryTotalTasks}} tasks closed). Points: {{userStoryPoints}}. Description: {{userStoryDescription}}",
+
"SECTION_NAME": "User story details",
"LINK_TASKBOARD": "Taskboard",
"TITLE_LINK_TASKBOARD": "Go to the taskboard",
@@ -772,6 +867,8 @@
}
},
"BACKLOG": {
+ "PAGE_TITLE": "Backlog - {{projectName}}",
+ "PAGE_DESCRIPTION": "The backlog panel, with user stories and sprints of the project {{projectName}}: {{projectDescription}}",
"SECTION_NAME": "Backlog",
"MOVE_US_TO_CURRENT_SPRINT": "Move to Current Sprint",
"SHOW_FILTERS": "Show filters",
@@ -852,6 +949,8 @@
"VERSION_ERROR": "Someone inside Taiga has changed this before and our Oompa Loompas cannot apply your changes. Please reload and apply your changes again (they will be lost)."
},
"TASKBOARD": {
+ "PAGE_TITLE": "{{sprintName}} - Sprint taskboard - {{projectName}}",
+ "PAGE_DESCRIPTION": "Sprint {{sprintName}} (from {{startDate}} to {{endDate}}) of {{projectName}}. Completed {{completedPercentage}}% ({{completedPoints}} of {{totalPoints}} points). {{openTasks}} opened tasks of {{totalTasks}}." ,
"SECTION_NAME": "Taskboard",
"TITLE_ACTION_ADD": "Add a new Task",
"TITLE_ACTION_ADD_BULK": "Add some new Tasks in bulk",
@@ -875,6 +974,8 @@
}
},
"TASK": {
+ "PAGE_TITLE": "{{taskSubject}} - Task {{taskRef}} - {{projectName}}",
+ "PAGE_DESCRIPTION": "Status: {{taskStatus }}. Description: {{taskDescription}}",
"SECTION_NAME": "Task details",
"LINK_TASKBOARD": "Taskboard",
"TITLE_LINK_TASKBOARD": "Go to the taskboard",
@@ -920,56 +1021,9 @@
"ACTION_CHANGE_EMAIL": "Change email",
"SUCCESS": "Our Oompa Loompas updated your email"
},
- "CHANGE_PASSWORD_RECOVERY_FORM": {
- "TITLE": "Create a new Taiga pass",
- "SUBTITLE": "And hey, you may want to eat some more iron-rich food, it's good for your brain :P",
- "PLACEHOLDER_RECOVER_PASSWORD_TOKEN": "Recover password token",
- "LINK_NEED_TOKEN": "Need one?",
- "TITLE_LINK_NEED_TOKEN": "Did you need a token to recover your password because you forgot it?",
- "PLACEHOLDER_NEW_PASSWORD": "New password",
- "PLACEHOLDER_RE_TYPE_NEW_PASSWORD": "Re-type new password",
- "ACTION_RESET_PASSWORD": "Reset Password",
- "SUCCESS": "Our Oompa Loompas saved your new password.
Try to sign in with it."
- },
- "FORGOT_PASSWORD_FORM": {
- "TITLE": "Oops, did you forget your password?",
- "SUBTITLE": "Enter your username or email to get a new one",
- "PLACEHOLDER_FIELD": "Username or email",
- "ACTION_RESET_PASSWORD": "Reset Password",
- "LINK_CANCEL": "Nah, take me back. I think I remember it.",
- "SUCCESS": "Check your inbox!
We have sent you an email with the instructions to set a new password",
- "ERROR": "According to our Oompa Loompas, your are not registered yet."
- },
- "LOGIN_COMMON": {
- "HEADER": "I already have a Taiga login",
- "PLACEHOLDER_AUTH_NAME": "Username or email (case sensitive)",
- "LINK_FORGOT_PASSWORD": "Forgot it?",
- "TITLE_LINK_FORGOT_PASSWORD": "Did you forgot your password?",
- "ACTION_ENTER": "Enter",
- "ACTION_SIGN_IN": "Sign in",
- "PLACEHOLDER_AUTH_PASSWORD": "Password (case sensitive)"
- },
- "LOGIN_FORM": {
- "ERROR_AUTH_INCORRECT": "According to our Oompa Loompas, your username/email or password are incorrect.",
- "ERROR_GENERIC": "According to our Oompa Loompas there was an error.",
- "SUCCESS": "Our Oompa Loompas are happy, welcome to Taiga."
- },
- "INVITATION_LOGIN_FORM": {
- "NOT_FOUND": "Ooops, we have a problem
Our Oompa Loompas can't find your invitation.",
- "SUCCESS": "You've successfully joined this project, Welcome to {{project_name}}",
- "ERROR": "According to our Oompa Loompas, your are not registered yet or typed an invalid password."
- },
- "REGISTER_FORM": {
- "TITLE": "Register a new Taiga account (free)",
- "PLACEHOLDER_NAME": "Pick a username (case sensitive)",
- "PLACEHOLDER_FULL_NAME": "Pick your full name",
- "PLACEHOLDER_EMAIL": "Your email",
- "PLACEHOLDER_PASSWORD": "Set a password (case sensitive)",
- "ACTION_SIGN_UP": "Sign up",
- "TITLE_LINK_LOGIN": "Log in",
- "LINK_LOGIN": "Are you already registered? Log in"
- },
"ISSUES": {
+ "PAGE_TITLE": "Issues - {{projectName}}",
+ "PAGE_DESCRIPTION": "The issues list panel of the project {{projectName}}: {{projectDescription}}",
"LIST_SECTION_NAME": "Issues",
"SECTION_NAME": "Issue details",
"ACTION_NEW_ISSUE": "+ NEW ISSUE",
@@ -1033,7 +1087,13 @@
}
}
},
+ "ISSUE": {
+ "PAGE_TITLE": "{{issueSubject}} - Issue {{issueRef}} - {{projectName}}",
+ "PAGE_DESCRIPTION": "Status: {{issueStatus }}. Type: {{issueType}}, Priority: {{issuePriority}}. Severity: {{issueSeverity}}. Description: {{issueDescription}}"
+ },
"KANBAN": {
+ "PAGE_TITLE": "Kanban - {{projectName}}",
+ "PAGE_DESCRIPTION": "The kanban panel, with user stories of the project {{projectName}}: {{projectDescription}}",
"SECTION_NAME": "Kanban",
"TITLE_ACTION_FOLD": "Fold column",
"TITLE_ACTION_UNFOLD": "Unfold column",
@@ -1048,6 +1108,8 @@
"UNDO_ARCHIVED": "Drag & drop again to undo"
},
"SEARCH": {
+ "PAGE_TITLE": "Search - {{projectName}}",
+ "PAGE_DESCRIPTION": "Search anything, user stories, issues, tasks or wiki pages, in the project {{projectName}}: {{projectDescription}}",
"FILTER_USER_STORIES": "User Stories",
"FILTER_ISSUES": "Issues",
"FILTER_TASKS": "Tasks",
@@ -1058,6 +1120,8 @@
"EMPTY_DESCRIPTION": "Maybe try one of the tabs above or search again"
},
"TEAM": {
+ "PAGE_TITLE": "Team - {{projectName}}",
+ "PAGE_DESCRIPTION": "The team panel to show all the members of the project {{projectName}}: {{projectDescription}}",
"SECTION_NAME": "Team",
"APP_TITLE": "TEAM - {{projectName}}",
"PLACEHOLDER_INPUT_SEARCH": "Search by full name...",
@@ -1078,16 +1142,6 @@
"CONFIRM_LEAVE_PROJECT": "Are you sure you want to leave the project?",
"ACTION_LEAVE_PROJECT": "Leave this project"
},
- "CHANGE_PASSWORD": {
- "SECTION_NAME": "Change password",
- "FIELD_CURRENT_PASSWORD": "Current password",
- "PLACEHOLDER_CURRENT_PASSWORD": "Your current password (or empty if you have no password yet)",
- "FIELD_NEW_PASSWORD": "New password",
- "PLACEHOLDER_NEW_PASSWORD": "Type a new password",
- "FIELD_RETYPE_PASSWORD": "Retype new password",
- "PLACEHOLDER_RETYPE_PASSWORD": "Retype the new password",
- "ERROR_PASSWORD_MATCH": "The passwords doesn't match"
- },
"USER_SETTINGS": {
"AVATAR_MAX_SIZE": "[Max. size: {{maxFileSize}}]",
"MENU": {
@@ -1141,6 +1195,8 @@
"PROGRESS_NAME_DESCRIPTION": "Name and description"
},
"WIKI": {
+ "PAGE_TITLE": "{{wikiPageName}} - Wiki - {{projectName}}",
+ "PAGE_DESCRIPTION": "Last edition on {{lastModifiedDate}} ({{totalEditions}} editions in total) Content: {{ wikiPageContent }}",
"DATETIME": "DD MMM YYYY HH:mm",
"PLACEHOLDER_PAGE": "Write your wiki page",
"REMOVE": "Remove this wiki page",
diff --git a/app/modules/profile/profile.controller.coffee b/app/modules/profile/profile.controller.coffee
index cca54f1c..e5ac1770 100644
--- a/app/modules/profile/profile.controller.coffee
+++ b/app/modules/profile/profile.controller.coffee
@@ -1,13 +1,14 @@
class ProfileController
@.$inject = [
- "$appTitle",
+ "tgAppMetaService",
"tgCurrentUserService",
"$routeParams",
"tgUserService",
- "tgXhrErrorService"
+ "tgXhrErrorService",
+ "$translate"
]
- constructor: (@appTitle, @currentUserService, @routeParams, @userService, @xhrError) ->
+ constructor: (@appMetaService, @currentUserService, @routeParams, @userService, @xhrError, @translate) ->
@.isCurrentUser = false
if @routeParams.slug
@@ -16,13 +17,21 @@ class ProfileController
.then (user) =>
@.user = user
@.isCurrentUser = false
- @appTitle.set(@.user.get('full_name'))
+ @._setMeta(@.user)
.catch (xhr) =>
@xhrError.response(xhr)
else
@.user = @currentUserService.getUser()
@.isCurrentUser = true
- @appTitle.set(@.user.get('full_name_display'))
+ @._setMeta(@.user)
+
+ _setMeta: (user) ->
+ title = @translate.instant("USER.PROFILE.PAGE_TITLE", {
+ userFullName: user.get("full_name_display"),
+ userUsername: user.get("username")
+ })
+ description = user.get("bio")
+ @appMetaService.setAll(title, description)
angular.module("taigaProfile").controller("Profile", ProfileController)
diff --git a/app/modules/profile/profile.controller.spec.coffee b/app/modules/profile/profile.controller.spec.coffee
index e624113a..d471f100 100644
--- a/app/modules/profile/profile.controller.spec.coffee
+++ b/app/modules/profile/profile.controller.spec.coffee
@@ -10,18 +10,21 @@ describe "ProfileController", ->
{id: 3}
])
- _mockAppTitle = () ->
- stub = sinon.stub()
-
- mocks.appTitle = {
- set: sinon.spy()
+ _mockTranslate = () ->
+ mocks.translate = {
+ instant: sinon.stub()
}
- provide.value "$appTitle", mocks.appTitle
+ provide.value "$translate", mocks.translate
+
+ _mockAppMetaService = () ->
+ mocks.appMetaService = {
+ setAll: sinon.spy()
+ }
+
+ provide.value "tgAppMetaService", mocks.appMetaService
_mockCurrentUser = () ->
- stub = sinon.stub()
-
mocks.currentUser = {
getUser: sinon.stub()
}
@@ -29,8 +32,6 @@ describe "ProfileController", ->
provide.value "tgCurrentUserService", mocks.currentUser
_mockUserService = () ->
- stub = sinon.stub()
-
mocks.userService = {
getUserByUserName: sinon.stub()
}
@@ -38,15 +39,11 @@ describe "ProfileController", ->
provide.value "tgUserService", mocks.userService
_mockRouteParams = () ->
- stub = sinon.stub()
-
mocks.routeParams = {}
provide.value "$routeParams", mocks.routeParams
_mockXhrErrorService = () ->
- stub = sinon.stub()
-
mocks.xhrErrorService = {
response: sinon.spy()
}
@@ -56,12 +53,12 @@ describe "ProfileController", ->
_mocks = () ->
module ($provide) ->
provide = $provide
- _mockAppTitle()
+ _mockTranslate()
+ _mockAppMetaService()
_mockCurrentUser()
_mockRouteParams()
_mockUserService()
_mockXhrErrorService()
-
return null
_inject = (callback) ->
@@ -81,9 +78,18 @@ describe "ProfileController", ->
mocks.routeParams.slug = "user-slug"
user = Immutable.fromJS({
- full_name: "full-name"
+ username: "username"
+ full_name_display: "full-name-display"
+ bio: "bio"
})
+ mocks.translate.instant
+ .withArgs('USER.PROFILE.PAGE_TITLE', {
+ userFullName: user.get("full_name_display"),
+ userUsername: user.get("username")
+ })
+ .returns('user-profile-page-title')
+
mocks.userService.getUserByUserName.withArgs(mocks.routeParams.slug).promise().resolve(user)
ctrl = $controller("Profile")
@@ -91,8 +97,7 @@ describe "ProfileController", ->
setTimeout ( ->
expect(ctrl.user).to.be.equal(user)
expect(ctrl.isCurrentUser).to.be.false
- expect(mocks.appTitle.set.calledWithExactly("full-name")).to.be.true
-
+ expect(mocks.appMetaService.setAll.calledWithExactly("user-profile-page-title", "bio")).to.be.true
done()
)
@@ -111,7 +116,6 @@ describe "ProfileController", ->
setTimeout ( ->
expect(mocks.xhrErrorService.response.withArgs(xhr)).to.be.calledOnce
-
done()
)
@@ -119,13 +123,22 @@ describe "ProfileController", ->
$scope = $rootScope.$new()
user = Immutable.fromJS({
+ username: "username"
full_name_display: "full-name-display"
+ bio: "bio"
})
+ mocks.translate.instant
+ .withArgs('USER.PROFILE.PAGE_TITLE', {
+ userFullName: user.get("full_name_display"),
+ userUsername: user.get("username")
+ })
+ .returns('user-profile-page-title')
+
mocks.currentUser.getUser.returns(user)
ctrl = $controller("Profile")
expect(ctrl.user).to.be.equal(user)
expect(ctrl.isCurrentUser).to.be.true
- expect(mocks.appTitle.set.calledWithExactly("full-name-display")).to.be.true
+ expect(mocks.appMetaService.setAll.withArgs("user-profile-page-title", "bio")).to.be.calledOnce
diff --git a/app/modules/projects/project/project.controller.coffee b/app/modules/projects/project/project.controller.coffee
index 2da83dc1..55816d52 100644
--- a/app/modules/projects/project/project.controller.coffee
+++ b/app/modules/projects/project/project.controller.coffee
@@ -2,22 +2,28 @@ class ProjectController
@.$inject = [
"tgProjectsService",
"$routeParams",
- "$appTitle",
+ "tgAppMetaService",
"$tgAuth",
- "tgXhrErrorService"
+ "tgXhrErrorService",
+ "$translate"
]
- constructor: (@projectsService, @routeParams, @appTitle, @auth, @xhrError) ->
+ constructor: (@projectsService, @routeParams, @appMetaService, @auth, @xhrError, @translate) ->
projectSlug = @routeParams.pslug
@.user = @auth.userData
@projectsService
.getProjectBySlug(projectSlug)
.then (project) =>
- @appTitle.set(project.get("name"))
-
@.project = project
+ @._setMeta(@.project)
+
.catch (xhr) =>
@xhrError.response(xhr)
+ _setMeta: (project)->
+ title = @translate.instant("PROJECT.PAGE_TITLE", {projectName: project.get("name")})
+ description = project.get("description")
+ @appMetaService.setAll(title, description)
+
angular.module("taigaProjects").controller("Project", ProjectController)
diff --git a/app/modules/projects/project/project.controller.spec.coffee b/app/modules/projects/project/project.controller.spec.coffee
index b28d7f87..03951199 100644
--- a/app/modules/projects/project/project.controller.spec.coffee
+++ b/app/modules/projects/project/project.controller.spec.coffee
@@ -12,12 +12,12 @@ describe "ProjectController", ->
provide.value "tgProjectsService", mocks.projectService
- _mockAppTitle = () ->
- mocks.appTitle = {
- set: sinon.stub()
+ _mockAppMetaService = () ->
+ mocks.appMetaService = {
+ setAll: sinon.stub()
}
- provide.value "$appTitle", mocks.appTitle
+ provide.value "tgAppMetaService", mocks.appMetaService
_mockAuth = () ->
mocks.auth = {
@@ -38,15 +38,22 @@ describe "ProjectController", ->
provide.value "tgXhrErrorService", mocks.xhrErrorService
+ _mockTranslate = () ->
+ mocks.translate = {
+ instant: sinon.stub()
+ }
+
+ provide.value "$translate", mocks.translate
+
_mocks = () ->
module ($provide) ->
provide = $provide
_mockProjectsService()
_mockRouteParams()
- _mockAppTitle()
+ _mockAppMetaService()
_mockAuth()
_mockXhrErrorService()
-
+ _mockTranslate()
return null
_inject = (callback) ->
@@ -73,16 +80,24 @@ describe "ProjectController", ->
expect(ctrl.user).to.be.equal(mocks.auth.userData)
it "set page title", (done) ->
+ $scope = $rootScope.$new()
project = Immutable.fromJS({
name: "projectName"
+ description: "projectDescription"
})
+ mocks.translate.instant
+ .withArgs('PROJECT.PAGE_TITLE', {
+ projectName: project.get("name")
+ })
+ .returns('projectTitle')
+
mocks.projectService.getProjectBySlug.withArgs("project-slug").promise().resolve(project)
ctrl = $controller("Project")
setTimeout ( ->
- expect(mocks.appTitle.set.withArgs("projectName")).to.be.calledOnce
+ expect(mocks.appMetaService.setAll.calledWithExactly("projectTitle", "projectDescription")).to.be.true
done()
)
diff --git a/app/modules/services/app-meta.service.coffee b/app/modules/services/app-meta.service.coffee
new file mode 100644
index 00000000..1b729006
--- /dev/null
+++ b/app/modules/services/app-meta.service.coffee
@@ -0,0 +1,63 @@
+taiga = @.taiga
+
+truncate = taiga.truncate
+
+
+class AppMetaService extends taiga.Service = ->
+ _set: (key, value) ->
+ return if not key
+
+ if key == "title"
+ meta = $("title")
+
+ if meta.length == 0
+ meta = $("