Taiga-events integration (realtime taiga)

stable
Andrey Antukh 2014-09-11 14:05:55 +02:00 committed by Jesús Espino
parent 56954aa1f0
commit 38a6f73b9f
11 changed files with 266 additions and 62 deletions

View File

@ -21,7 +21,21 @@
@taiga = taiga = {} @taiga = taiga = {}
configure = ($routeProvider, $locationProvider, $httpProvider, $provide, tgLoaderProvider) -> # Generic function for generate hash from a arbitrary length
# collection of parameters.
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) ->
$routeProvider.when("/", $routeProvider.when("/",
{templateUrl: "/partials/projects.html", resolve: {loader: tgLoaderProvider.add()}}) {templateUrl: "/partials/projects.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/", $routeProvider.when("/project/:pslug/",
@ -127,13 +141,18 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, tgLoade
defaultHeaders = { defaultHeaders = {
"Content-Type": "application/json" "Content-Type": "application/json"
"Accept-Language": "en" "Accept-Language": "en"
"X-Session-Id": taiga.sessionId
} }
$httpProvider.defaults.headers.delete = defaultHeaders $httpProvider.defaults.headers.delete = defaultHeaders
$httpProvider.defaults.headers.patch = defaultHeaders $httpProvider.defaults.headers.patch = defaultHeaders
$httpProvider.defaults.headers.post = defaultHeaders $httpProvider.defaults.headers.post = defaultHeaders
$httpProvider.defaults.headers.put = defaultHeaders $httpProvider.defaults.headers.put = defaultHeaders
$httpProvider.defaults.headers.get = {} $httpProvider.defaults.headers.get = {
"X-Session-Id": taiga.sessionId
}
$tgEventsProvider.setSessionId(taiga.sessionId)
# Add next param when user try to access to a secction need auth permissions. # Add next param when user try to access to a secction need auth permissions.
authHttpIntercept = ($q, $location, $confirm, $navUrls, $lightboxService) -> authHttpIntercept = ($q, $location, $confirm, $navUrls, $lightboxService) ->
@ -148,7 +167,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, tgLoade
$location.url($navUrls.resolve("login")).search("next=#{nextPath}") $location.url($navUrls.resolve("login")).search("next=#{nextPath}")
return $q.reject(response) return $q.reject(response)
$provide.factory("authHttpIntercept", ["$q", "$location", "$tgConfirm", "$tgNavUrls", "lightboxService", authHttpIntercept]) $provide.factory("authHttpIntercept", ["$q", "$location", "$tgConfirm", "$tgNavUrls",
"lightboxService", authHttpIntercept])
$httpProvider.responseInterceptors.push('authHttpIntercept') $httpProvider.responseInterceptors.push('authHttpIntercept')
$httpProvider.interceptors.push('loaderInterceptor') $httpProvider.interceptors.push('loaderInterceptor')
@ -166,10 +186,13 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, tgLoade
linewidth: "The subject must have a maximum size of %s" linewidth: "The subject must have a maximum size of %s"
}) })
init = ($log, $i18n, $config, $rootscope) -> init = ($log, $i18n, $config, $rootscope, $auth, $events) ->
$i18n.initialize($config.get("defaultLanguage")) $i18n.initialize($config.get("defaultLanguage"))
$log.debug("Initialize application") $log.debug("Initialize application")
if $auth.isAuthenticated()
$events.setupConnection()
# Default Value for taiga local config module. # Default Value for taiga local config module.
angular.module("taigaLocalConfig", []).value("localconfig", {}) angular.module("taigaLocalConfig", []).value("localconfig", {})
@ -181,6 +204,7 @@ modules = [
"taigaResources", "taigaResources",
"taigaLocales", "taigaLocales",
"taigaAuth", "taigaAuth",
"taigaEvents",
# Specific Modules # Specific Modules
"taigaRelatedTasks", "taigaRelatedTasks",
@ -211,6 +235,7 @@ module.config([
"$locationProvider", "$locationProvider",
"$httpProvider", "$httpProvider",
"$provide", "$provide",
"$tgEventsProvider",
"tgLoaderProvider", "tgLoaderProvider",
configure configure
]) ])
@ -220,5 +245,7 @@ module.run([
"$tgI18n", "$tgI18n",
"$tgConfig", "$tgConfig",
"$rootScope", "$rootScope",
"$tgAuth",
"$tgEvents",
init init
]) ])

View File

@ -171,7 +171,7 @@ PublicRegisterMessageDirective = ($config, $navUrls) ->
module.directive("tgPublicRegisterMessage", ["$tgConfig", "$tgNavUrls", PublicRegisterMessageDirective]) module.directive("tgPublicRegisterMessage", ["$tgConfig", "$tgNavUrls", PublicRegisterMessageDirective])
LoginDirective = ($auth, $confirm, $location, $routeParams, $navUrls) -> LoginDirective = ($auth, $confirm, $location, $config, $routeParams, $navUrls, $events) ->
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$scope.data = {} $scope.data = {}
@ -181,6 +181,7 @@ LoginDirective = ($auth, $confirm, $location, $routeParams, $navUrls) ->
else else
nextUrl = $navUrls.resolve("home") nextUrl = $navUrls.resolve("home")
$events.setupConnection()
$location.path(nextUrl) $location.path(nextUrl)
onErrorSubmit = (response) -> onErrorSubmit = (response) ->
@ -204,8 +205,8 @@ LoginDirective = ($auth, $confirm, $location, $routeParams, $navUrls) ->
return {link:link} return {link:link}
module.directive("tgLogin", ["$tgAuth", "$tgConfirm", "$tgLocation", "$routeParams", "$tgNavUrls", module.directive("tgLogin", ["$tgAuth", "$tgConfirm", "$tgLocation", "$tgConfig", "$routeParams",
LoginDirective]) "$tgNavUrls", "$tgEvents", LoginDirective])
############################################################################# #############################################################################
## Register Directive ## Register Directive

View File

@ -46,11 +46,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
"$tgLocation", "$tgLocation",
"$appTitle", "$appTitle",
"$tgNavUrls", "$tgNavUrls",
"$tgEvents",
"tgLoader" "tgLoader"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appTitle, @navUrls, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q,
tgLoader) -> @location, @appTitle, @navUrls, @events, tgLoader) ->
_.bindAll(@) _.bindAll(@)
@scope.sectionName = "Backlog" @scope.sectionName = "Backlog"
@ -94,8 +95,18 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
@scope.$on("sprint:us:moved", @.loadSprints) @scope.$on("sprint:us:moved", @.loadSprints)
@scope.$on("sprint:us:moved", @.loadProjectStats) @scope.$on("sprint:us:moved", @.loadProjectStats)
initializeSubscription: ->
routingKey1 = "changes.project.#{@scope.projectId}.userstories"
@events.subscribe @scope, routingKey1, (message) =>
@.loadUserstories()
@.loadSprints()
routingKey2 = "changes.project.#{@scope.projectId}.milestones"
@events.subscribe @scope, routingKey2, (message) =>
@.loadSprints()
toggleShowTags: -> toggleShowTags: ->
@scope.$apply () => @scope.$apply =>
@showTags = !@showTags @showTags = !@showTags
@rs.userstories.storeShowTags(@scope.projectId, @showTags) @rs.userstories.storeShowTags(@scope.projectId, @showTags)
@ -186,6 +197,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# Resolve project slug # Resolve project slug
promise = @repo.resolve({pslug: @params.pslug}).then (data) => promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project @scope.projectId = data.project
@.initializeSubscription()
return data return data
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())

View File

@ -333,15 +333,20 @@ ListItemPriorityDirective = ->
""" """
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
issue = $scope.$eval($attrs.tgListitemPriority) render = (priorityById, issue) ->
bindOnce $scope, "priorityById", (priorityById) ->
priority = priorityById[issue.priority] priority = priorityById[issue.priority]
domNode = $el.find(".level")
domNode = $el.find("div.level")
domNode.css("background-color", priority.color) domNode.css("background-color", priority.color)
domNode.addClass(priority.name.toLowerCase()) domNode.addClass(priority.name.toLowerCase())
domNode.attr("title", priority.name) domNode.attr("title", priority.name)
bindOnce $scope, "priorityById", (priorityById) ->
issue = $scope.$eval($attrs.tgListitemPriority)
render(priorityById, issue)
$scope.$watch $attrs.tgListitemPriority, (issue) ->
render($scope.priorityById, issue)
return { return {
link: link link: link
template: template template: template
@ -354,15 +359,20 @@ ListItemSeverityDirective = ->
""" """
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
issue = $scope.$eval($attrs.tgListitemSeverity) render = (severityById, issue) ->
bindOnce $scope, "severityById", (severityById) ->
severity = severityById[issue.severity] severity = severityById[issue.severity]
domNode = $el.find(".level")
domNode = $el.find("div.level")
domNode.css("background-color", severity.color) domNode.css("background-color", severity.color)
domNode.addClass(severity.name.toLowerCase()) domNode.addClass(severity.name.toLowerCase())
domNode.attr("title", severity.name) domNode.attr("title", severity.name)
bindOnce $scope, "severityById", (severityById) ->
issue = $scope.$eval($attrs.tgListitemSeverity)
render(severityById, issue)
$scope.$watch $attrs.tgListitemSeverity, (issue) ->
render($scope.severityById, issue)
return { return {
link: link link: link
template: template template: template
@ -374,16 +384,20 @@ ListItemTypeDirective = ->
""" """
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
issue = $scope.$eval($attrs.tgListitemType) render = (issueTypeById, issue) ->
bindOnce $scope, "issueTypeById", (issueTypeById) ->
type = issueTypeById[issue.type] type = issueTypeById[issue.type]
domNode = $el.find(".level")
domNode = $el.find("div.level")
domNode.css("background-color", type.color) domNode.css("background-color", type.color)
domNode.addClass(type.name.toLowerCase()) domNode.addClass(type.name.toLowerCase())
domNode.attr("title", type.name) domNode.attr("title", type.name)
bindOnce $scope, "issueTypeById", (issueTypeById) ->
issue = $scope.$eval($attrs.tgListitemType)
render(issueTypeById, issue)
$scope.$watch $attrs.tgListitemType, (issue) ->
render($scope.issueTypeById, issue)
return { return {
link: link link: link
template: template template: template

View File

@ -42,9 +42,9 @@ UsStatusDirective = ($repo, popoverService) ->
NOTE: This directive need 'usStatusById' and 'project'. NOTE: This directive need 'usStatusById' and 'project'.
### ###
selectionTemplate = _.template(""" template = _.template("""
<ul class="popover pop-status"> <ul class="popover pop-status">
<% _.forEach(statuses, function(status) { %> <% _.each(statuses, function(status) { %>
<li> <li>
<a href="" class="status" title="<%- status.name %>" data-status-id="<%- status.id %>"> <a href="" class="status" title="<%- status.name %>" data-status-id="<%- status.id %>">
<%- status.name %> <%- status.name %>
@ -53,51 +53,56 @@ UsStatusDirective = ($repo, popoverService) ->
<% }); %> <% }); %>
</ul>""") </ul>""")
updateUsStatus = ($el, us, usStatusById) ->
usStatusDomParent = $el.find(".us-status")
usStatusDom = $el.find(".us-status .us-status-bind")
if usStatusById[us.status]
usStatusDom.text(usStatusById[us.status].name)
usStatusDomParent.prop("title", usStatusById[us.status].name)
usStatusDomParent.css('color', usStatusById[us.status].color)
link = ($scope, $el, $attrs) -> link = ($scope, $el, $attrs) ->
$ctrl = $el.controller() $ctrl = $el.controller()
us = $scope.$eval($attrs.tgUsStatus)
render = (us) ->
usStatusDomParent = $el.find(".us-status")
usStatusDom = $el.find(".us-status .us-status-bind")
usStatusById = $scope.usStatusById
if usStatusById[us.status]
usStatusDom.text(usStatusById[us.status].name)
usStatusDomParent.css("color", usStatusById[us.status].color)
$el.on "click", ".us-status", (event) -> $el.on "click", ".us-status", (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
$el.find(".pop-status").popover().open() $el.find(".pop-status").popover().open()
# pop = $el.find(".pop-status")
# popoverService.open(pop)
$el.on "click", ".status", debounce 2000, (event) -> $el.on "click", ".status", debounce 2000, (event) ->
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
target = angular.element(event.currentTarget) target = angular.element(event.currentTarget)
us = $scope.$eval($attrs.tgUsStatus)
us.status = target.data("status-id") us.status = target.data("status-id")
render(us)
$el.find(".pop-status").popover().close() $el.find(".pop-status").popover().close()
updateUsStatus($el, us, $scope.usStatusById)
$scope.$apply () -> $scope.$apply () ->
$repo.save(us).then -> $repo.save(us).then ->
$scope.$eval($attrs.onUpdate) $scope.$eval($attrs.onUpdate)
taiga.bindOnce $scope, "project", (project) ->
$el.append(selectionTemplate({ 'statuses': project.us_statuses })) $scope.$on("userstories:loaded", -> render($scope.$eval($attrs.tgUsStatus)))
updateUsStatus($el, us, $scope.usStatusById) $scope.$on("$destroy", -> $el.off())
# Bootstrap
us = $scope.$eval($attrs.tgUsStatus)
render(us)
bindOnce $scope, "project", (project) ->
html = template({"statuses": project.us_statuses})
$el.append(html)
# If the user has not enough permissions the click events are unbinded # If the user has not enough permissions the click events are unbinded
if project.my_permissions.indexOf("modify_us") == -1 if $scope.project.my_permissions.indexOf("modify_us") == -1
$el.unbind("click") $el.unbind("click")
$el.find("a").addClass("not-clickable") $el.find("a").addClass("not-clickable")
$scope.$on "$destroy", ->
$el.off()
return {link: link} return {link: link}

View File

@ -0,0 +1,129 @@
###
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
# Copyright (C) 2014 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014 David Barragán Merino <bameda@dbarragan.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# File: modules/events.coffee
###
taiga = @.taiga
module = angular.module("taigaEvents", [])
class EventsService
constructor: (@win, @log, @config, @auth) ->
_.bindAll(@)
initialize: (sessionId) ->
@.sessionId = sessionId
@.subscriptions = {}
if @win.WebSocket is undefined
@log.debug "WebSockets not supported on your browser"
setupConnection: ->
@.stopExistingConnection()
wshost = @config.get("eventsHost", "localhost:8888")
wsscheme = @config.get("eventsScheme", "ws")
url = "#{wsscheme}://#{wshost}/events"
@.ws = new @win.WebSocket(url)
@.ws.addEventListener("open", @.onOpen)
@.ws.addEventListener("message", @.onMessage)
@.ws.addEventListener("error", @.onError)
@.ws.addEventListener("close", @.onClose)
stopExistingConnection: ->
if @.ws is undefined
return
@.ws.close()
@.ws.removeEventListener("open", @.onOpen)
@.ws.removeEventListener("close", @.onClose)
@.ws.removeEventListener("error", @.onError)
@.ws.removeEventListener("message", @.onMessage)
delete @.ws
onOpen: ->
@log.debug("WebSocket connection opened")
token = @auth.getToken()
message = {
cmd: "auth"
data: {token: token, sessionId: @.sessionId}
}
@.ws.send(JSON.stringify(message))
onMessage: (event) ->
@.log.debug "WebSocket message received: #{event.data}"
data = JSON.parse(event.data)
routingKey = data.routing_key
if not @.subscriptions[routingKey]?
return
subscription = @.subscriptions[routingKey]
subscription.scope.$apply ->
subscription.callback(data.data)
onError: (error) ->
@log.error("WebSocket error: #{error}")
onClose: ->
@log.debug("WebSocket closed.")
subscribe: (scope, routingKey, callback) ->
subscription = {
scope: scope,
routingKey: routingKey,
callback: callback
}
message = {
"cmd": "subscribe",
"routing_key": routingKey
}
@.subscriptions[routingKey] = subscription
@.ws.send(JSON.stringify(message))
scope.$on("$destroy", => @.unsubscribe(routingKey))
unsubscribe: (routingKey) ->
message = {
"cmd": "unsubscribe",
"routing_key": routingKey
}
@.ws.send(JSON.stringify(message))
class EventsProvider
setSessionId: (sessionId) ->
@.sessionId = sessionId
$get: ($win, $log, $conf, $auth) ->
service = new EventsService($win, $log, $conf, $auth)
service.initialize(@.sessionId)
return service
@.prototype.$get.$inject = ["$window", "$log", "$tgConfig", "$tgAuth"]
module.provider("$tgEvents", EventsProvider)

View File

@ -49,11 +49,12 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
"$tgLocation", "$tgLocation",
"$appTitle", "$appTitle",
"$tgNavUrls", "$tgNavUrls",
"$tgEvents",
"tgLoader" "tgLoader"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appTitle, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appTitle,
@navUrls, tgLoader) -> @navUrls, @events, tgLoader) ->
@scope.sectionName = "Issues" @scope.sectionName = "Issues"
@scope.filters = {} @scope.filters = {}
@ -82,6 +83,11 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@.loadIssues() @.loadIssues()
@.loadFilters() @.loadFilters()
initializeSubscription: ->
routingKey = "changes.project.#{@scope.projectId}.issues"
@events.subscribe @scope, routingKey, (message) =>
@.loadIssues()
storeFilters: -> storeFilters: ->
@rs.issues.storeFilters(@params.pslug, @location.search()) @rs.issues.storeFilters(@params.pslug, @location.search())
@ -256,6 +262,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
loadInitialData: -> loadInitialData: ->
promise = @repo.resolve({pslug: @params.pslug}).then (data) => promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project @scope.projectId = data.project
@.initializeSubscription()
return data return data
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())
@ -755,6 +762,9 @@ IssueStatusInlineEditionDirective = ($repo, popoverService) ->
$el.unbind("click") $el.unbind("click")
$el.find("a").addClass("not-clickable") $el.find("a").addClass("not-clickable")
$scope.$watch $attrs.tgIssueStatusInlineEdition, (val) =>
updateIssueStatus($el, val, $scope.issueStatusById)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()
@ -803,6 +813,9 @@ IssueAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) ->
$repo.save(updatedIssue) $repo.save(updatedIssue)
updateIssue(updatedIssue) updateIssue(updatedIssue)
$scope.$watch $attrs.tgIssueAssignedToInlineEdition, (val) =>
updateIssue(val)
$scope.$on "$destroy", -> $scope.$on "$destroy", ->
$el.off() $el.off()

View File

@ -59,11 +59,12 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
"$tgLocation", "$tgLocation",
"$appTitle", "$appTitle",
"$tgNavUrls", "$tgNavUrls",
"$tgEvents",
"tgLoader" "tgLoader"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appTitle, @navUrls, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
tgLoader) -> @appTitle, @navUrls, @events, tgLoader) ->
_.bindAll(@) _.bindAll(@)
@scope.sectionName = "Kanban" @scope.sectionName = "Kanban"
@scope.statusViewModes = {} @scope.statusViewModes = {}
@ -162,10 +163,16 @@ class KanbanController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@scope.$emit("project:loaded", project) @scope.$emit("project:loaded", project)
return project return project
initializeSubscription: ->
routingKey1 = "changes.project.#{@scope.projectId}.userstories"
@events.subscribe @scope, routingKey1, (message) =>
@.loadUserstories()
loadInitialData: -> loadInitialData: ->
# Resolve project slug # Resolve project slug
promise = @repo.resolve({pslug: @params.pslug}).then (data) => promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project @scope.projectId = data.project
@.initializeSubscription()
return data return data
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())

View File

@ -46,11 +46,12 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
"$appTitle", "$appTitle",
"$tgLocation", "$tgLocation",
"$tgNavUrls" "$tgNavUrls"
"$tgEvents"
"tgLoader" "tgLoader"
] ]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appTitle, @location, @navUrls, constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @appTitle, @location, @navUrls,
tgLoader) -> @events, tgLoader) ->
_.bindAll(@) _.bindAll(@)
@scope.sectionName = "Taskboard" @scope.sectionName = "Taskboard"
@ -82,6 +83,11 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
promise.then null, -> promise.then null, ->
console.log "FAIL" # TODO console.log "FAIL" # TODO
initializeSubscription: ->
routingKey = "changes.project.#{@scope.projectId}.tasks"
@events.subscribe @scope, routingKey, (message) =>
@.loadTaskboard()
loadProject: -> loadProject: ->
return @rs.projects.get(@scope.projectId).then (project) => return @rs.projects.get(@scope.projectId).then (project) =>
@scope.project = project @scope.project = project
@ -157,6 +163,7 @@ class TaskboardController extends mixOf(taiga.Controller, taiga.PageMixin)
promise = @repo.resolve(params).then (data) => promise = @repo.resolve(params).then (data) =>
@scope.projectId = data.project @scope.projectId = data.project
@scope.sprintId = data.milestone @scope.sprintId = data.milestone
@.initializeSubscription()
return data return data
return promise.then(=> @.loadProject()) return promise.then(=> @.loadProject())

View File

@ -125,15 +125,6 @@ sizeFormat = (input, precision=1) ->
return "#{size} #{units[number]}" return "#{size} #{units[number]}"
typeIsArray = Array.isArray || ( value ) -> return {}.toString.call( value ) is '[object Array]'
# Generic method for generate hash from a arbitrary length
# collection of parameters.
generateHash = (components=[]) ->
components = _.map(components, (x) -> JSON.stringify(x))
return hex_sha1(components.join(":"))
taiga = @.taiga taiga = @.taiga
taiga.nl2br = nl2br taiga.nl2br = nl2br
taiga.bindOnce = bindOnce taiga.bindOnce = bindOnce
@ -151,5 +142,3 @@ taiga.joinStr = joinStr
taiga.debounce = debounce taiga.debounce = debounce
taiga.startswith = startswith taiga.startswith = startswith
taiga.sizeFormat = sizeFormat taiga.sizeFormat = sizeFormat
taiga.typeIsArray = typeIsArray
taiga.generateHash = generateHash

View File

@ -14,7 +14,7 @@ section.issues-table.basic-table(ng-class="{empty: !issues.length}")
div.subject div.subject
a(href="", tg-nav="project-issues-detail:project=project.slug,ref=issue.ref") a(href="", tg-nav="project-issues-detail:project=project.slug,ref=issue.ref")
span(tg-bo-ref="issue.ref") span(tg-bo-ref="issue.ref")
span(tg-bo-bind="issue.subject") span(ng-bind="issue.subject")
div.issue-field(tg-issue-status-inline-edition="issue") div.issue-field(tg-issue-status-inline-edition="issue")
a.issue-status(href="", title="Change status") a.issue-status(href="", title="Change status")