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 = {}
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("/",
{templateUrl: "/partials/projects.html", resolve: {loader: tgLoaderProvider.add()}})
$routeProvider.when("/project/:pslug/",
@ -127,13 +141,18 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, tgLoade
defaultHeaders = {
"Content-Type": "application/json"
"Accept-Language": "en"
"X-Session-Id": taiga.sessionId
}
$httpProvider.defaults.headers.delete = defaultHeaders
$httpProvider.defaults.headers.patch = defaultHeaders
$httpProvider.defaults.headers.post = 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.
authHttpIntercept = ($q, $location, $confirm, $navUrls, $lightboxService) ->
@ -148,7 +167,8 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, tgLoade
$location.url($navUrls.resolve("login")).search("next=#{nextPath}")
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.interceptors.push('loaderInterceptor')
@ -166,10 +186,13 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, tgLoade
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"))
$log.debug("Initialize application")
if $auth.isAuthenticated()
$events.setupConnection()
# Default Value for taiga local config module.
angular.module("taigaLocalConfig", []).value("localconfig", {})
@ -181,6 +204,7 @@ modules = [
"taigaResources",
"taigaLocales",
"taigaAuth",
"taigaEvents",
# Specific Modules
"taigaRelatedTasks",
@ -211,6 +235,7 @@ module.config([
"$locationProvider",
"$httpProvider",
"$provide",
"$tgEventsProvider",
"tgLoaderProvider",
configure
])
@ -220,5 +245,7 @@ module.run([
"$tgI18n",
"$tgConfig",
"$rootScope",
"$tgAuth",
"$tgEvents",
init
])

View File

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

View File

@ -46,11 +46,12 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
"$tgLocation",
"$appTitle",
"$tgNavUrls",
"$tgEvents",
"tgLoader"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @appTitle, @navUrls,
tgLoader) ->
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q,
@location, @appTitle, @navUrls, @events, tgLoader) ->
_.bindAll(@)
@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", @.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: ->
@scope.$apply () =>
@scope.$apply =>
@showTags = !@showTags
@rs.userstories.storeShowTags(@scope.projectId, @showTags)
@ -186,6 +197,7 @@ class BacklogController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.F
# Resolve project slug
promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project
@.initializeSubscription()
return data
return promise.then(=> @.loadProject())

View File

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

View File

@ -42,9 +42,9 @@ UsStatusDirective = ($repo, popoverService) ->
NOTE: This directive need 'usStatusById' and 'project'.
###
selectionTemplate = _.template("""
template = _.template("""
<ul class="popover pop-status">
<% _.forEach(statuses, function(status) { %>
<% _.each(statuses, function(status) { %>
<li>
<a href="" class="status" title="<%- status.name %>" data-status-id="<%- status.id %>">
<%- status.name %>
@ -53,51 +53,56 @@ UsStatusDirective = ($repo, popoverService) ->
<% }); %>
</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) ->
$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) ->
event.preventDefault()
event.stopPropagation()
$el.find(".pop-status").popover().open()
# pop = $el.find(".pop-status")
# popoverService.open(pop)
$el.on "click", ".status", debounce 2000, (event) ->
event.preventDefault()
event.stopPropagation()
target = angular.element(event.currentTarget)
us = $scope.$eval($attrs.tgUsStatus)
us.status = target.data("status-id")
render(us)
$el.find(".pop-status").popover().close()
updateUsStatus($el, us, $scope.usStatusById)
$scope.$apply () ->
$repo.save(us).then ->
$scope.$eval($attrs.onUpdate)
taiga.bindOnce $scope, "project", (project) ->
$el.append(selectionTemplate({ 'statuses': project.us_statuses }))
updateUsStatus($el, us, $scope.usStatusById)
$scope.$on("userstories:loaded", -> render($scope.$eval($attrs.tgUsStatus)))
$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 project.my_permissions.indexOf("modify_us") == -1
if $scope.project.my_permissions.indexOf("modify_us") == -1
$el.unbind("click")
$el.find("a").addClass("not-clickable")
$scope.$on "$destroy", ->
$el.off()
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",
"$appTitle",
"$tgNavUrls",
"$tgEvents",
"tgLoader"
]
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @urls, @params, @q, @location, @appTitle,
@navUrls, tgLoader) ->
@navUrls, @events, tgLoader) ->
@scope.sectionName = "Issues"
@scope.filters = {}
@ -82,6 +83,11 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
@.loadIssues()
@.loadFilters()
initializeSubscription: ->
routingKey = "changes.project.#{@scope.projectId}.issues"
@events.subscribe @scope, routingKey, (message) =>
@.loadIssues()
storeFilters: ->
@rs.issues.storeFilters(@params.pslug, @location.search())
@ -256,6 +262,7 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
loadInitialData: ->
promise = @repo.resolve({pslug: @params.pslug}).then (data) =>
@scope.projectId = data.project
@.initializeSubscription()
return data
return promise.then(=> @.loadProject())
@ -755,6 +762,9 @@ IssueStatusInlineEditionDirective = ($repo, popoverService) ->
$el.unbind("click")
$el.find("a").addClass("not-clickable")
$scope.$watch $attrs.tgIssueStatusInlineEdition, (val) =>
updateIssueStatus($el, val, $scope.issueStatusById)
$scope.$on "$destroy", ->
$el.off()
@ -803,6 +813,9 @@ IssueAssignedToInlineEditionDirective = ($repo, $rootscope, popoverService) ->
$repo.save(updatedIssue)
updateIssue(updatedIssue)
$scope.$watch $attrs.tgIssueAssignedToInlineEdition, (val) =>
updateIssue(val)
$scope.$on "$destroy", ->
$el.off()

View File

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

View File

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

View File

@ -125,15 +125,6 @@ sizeFormat = (input, precision=1) ->
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.nl2br = nl2br
taiga.bindOnce = bindOnce
@ -151,5 +142,3 @@ taiga.joinStr = joinStr
taiga.debounce = debounce
taiga.startswith = startswith
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
a(href="", tg-nav="project-issues-detail:project=project.slug,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")
a.issue-status(href="", title="Change status")