diff --git a/app/coffee/modules/controllerMixins.coffee b/app/coffee/modules/controllerMixins.coffee
index da19ece4..10dccc59 100644
--- a/app/coffee/modules/controllerMixins.coffee
+++ b/app/coffee/modules/controllerMixins.coffee
@@ -76,6 +76,10 @@ class FiltersMixin
location = if load then @location else @location.noreload(@scope)
location.search(name, value)
+ replaceAllFilters: (filters, load=false) ->
+ location = if load then @location else @location.noreload(@scope)
+ location.search(filters)
+
unselectFilter: (name, value, load=false) ->
params = @location.search()
diff --git a/app/coffee/modules/issues/list.coffee b/app/coffee/modules/issues/list.coffee
index 90737f11..a732d925 100644
--- a/app/coffee/modules/issues/list.coffee
+++ b/app/coffee/modules/issues/list.coffee
@@ -108,115 +108,97 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
filters = _.pick(@location.search(), name)
return filters[name]
+ loadMyFilters: ->
+ deferred = @q.defer()
+ promise = @rs.issues.getMyFilters(@scope.projectId)
+ promise.then (filters) ->
+ result = _.map filters, (value, key) =>
+ obj = {
+ id: key,
+ name: key,
+ type: "myFilters"
+ }
+ obj.selected = false
+ return obj
+ deferred.resolve(result)
+ return deferred.promise
+
+ markSelectedFilters: (filters, urlfilters) ->
+ # Build selected filters (from url) fast lookup data structure
+ searchdata = {}
+ for name, value of _.omit(urlfilters, "page", "orderBy")
+ if not searchdata[name]?
+ searchdata[name] = {}
+
+ for val in "#{value}".split(",")
+ searchdata[name][val] = true
+
+ isSelected = (type, id) ->
+ if searchdata[type]? and searchdata[type][id]
+ return true
+ return false
+
+ for key, value of filters
+ for obj in value
+ obj.selected = if isSelected(obj.type, obj.id) then true else undefined
+
loadFilters: ->
- # This function is executed only once when page is loads and
- # it needs create all filters structure and know that
- # filters are selected from url params.
+ deferred = @q.defer()
urlfilters = @.getUrlFilters()
if urlfilters.subject
@scope.filtersSubject = urlfilters.subject
- return @rs.issues.filtersData(@scope.projectId).then (data) =>
- # Build selected filters (from url) fast lookup data structure
- searchdata = {}
+ @.loadMyFilters().then (myFilters) =>
+ @scope.filters.myFilters = myFilters
+ @rs.issues.filtersData(@scope.projectId).then (data) =>
+ usersFiltersFormat = (users, type, unknownOption) =>
+ reformatedUsers = _.map users, (t) =>
+ return {
+ id: t[0],
+ count: t[1],
+ type: type
+ name: if t[0] then @scope.usersById[t[0]].full_name_display else unknownOption
+ }
+ unknownItem = _.remove(reformatedUsers, (u) -> not u.id)
+ reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase())
+ if unknownItem.length > 0
+ reformatedUsers.unshift(unknownItem[0])
+ return reformatedUsers
- for name, value of _.omit(urlfilters, "page", "orderBy")
- # if name == "page" or name == "orderBy"
- # continue
- if not searchdata[name]?
- searchdata[name] = {}
+ choicesFiltersFormat = (choices, type, byIdObject) =>
+ _.map choices, (t) ->
+ return {
+ id: t[0],
+ name: byIdObject[t[0]].name,
+ color: byIdObject[t[0]].color,
+ count: t[1],
+ type: type}
- for val in "#{value}".split(",")
- searchdata[name][val] = true
+ tagsFilterFormat = (tags) =>
+ return _.map tags, (t) =>
+ return {
+ id: t[0],
+ name: t[0],
+ color: @scope.project.tags_colors[t[0]],
+ count: t[1],
+ type: "tags"
+ }
- isSelected = (type, id) ->
- if searchdata[type]? and searchdata[type][id]
- return true
- return false
+ # Build filters data structure
+ @scope.filters.statuses = choicesFiltersFormat data.statuses, "statuses", @scope.issueStatusById
+ @scope.filters.severities = choicesFiltersFormat data.severities, "severities", @scope.severityById
+ @scope.filters.priorities = choicesFiltersFormat data.priorities, "priorities", @scope.priorityById
+ @scope.filters.assignedTo = usersFiltersFormat data.assigned_to, "assignedTo", "Unassigned"
+ @scope.filters.createdBy = usersFiltersFormat data.created_by, "createdBy", "Unknown"
+ @scope.filters.types = choicesFiltersFormat data.types, "types", @scope.issueTypeById
+ @scope.filters.tags = tagsFilterFormat data.tags
- usersFiltersFormat = (users, type, unknownOption) =>
- reformatedUsers = _.map users, (t) =>
- obj = {
- id: t[0],
- count: t[1],
- type: type
- }
- if t[0]
- obj.name = @scope.usersById[t[0]].full_name_display
- else
- obj.name = unknownOption
+ @.markSelectedFilters(@scope.filters, urlfilters)
- obj.selected = true if isSelected(type, obj.id)
- return obj
- unknownItem = _.remove(reformatedUsers, (u) -> not u.id)
- reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase())
- if unknownItem.length > 0
- reformatedUsers.unshift(unknownItem[0])
- return reformatedUsers
-
-
- # Build filters data structure
- @scope.filters.statuses = _.map data.statuses, (t) =>
- obj = {
- id: t[0],
- name: @scope.issueStatusById[t[0]].name,
- color: @scope.issueStatusById[t[0]].color,
- count: t[1],
- type: "statuses"}
- obj.selected = true if isSelected("statuses", obj.id)
- return obj
-
- @scope.filters.severities = _.map data.severities, (t) =>
- obj = {
- id: t[0],
- name: @scope.severityById[t[0]].name,
- color: @scope.severityById[t[0]].color,
- count: t[1],
- type: "severities"
- }
- obj.selected = true if isSelected("severities", obj.id)
- return obj
-
- @scope.filters.priorities = _.map data.priorities, (t) =>
- obj = {
- id: t[0],
- name: @scope.priorityById[t[0]].name,
- color: @scope.priorityById[t[0]].color
- count: t[1],
- type: "priorities"
- }
- obj.selected = true if isSelected("priorities", obj.id)
- return obj
-
- @scope.filters.assignedTo = usersFiltersFormat data.assigned_to, "assignedTo", "Unassigned"
-
- @scope.filters.createdBy = usersFiltersFormat data.created_by, "createdBy", "Unknown"
-
- @scope.filters.tags = _.map data.tags, (t) =>
- obj = {
- id: t[0],
- name: t[0],
- color: @scope.project.tags_colors[t[0]],
- count: t[1],
- type: "tags"
- }
- obj.selected = true if isSelected("tags", obj.id)
- return obj
-
- @scope.filters.types = _.map data.types, (t) =>
- obj = {
- id: t[0],
- name: @scope.issueTypeById[t[0]].name,
- color: @scope.issueTypeById[t[0]].color
- count: t[1],
- type: "types"
- }
- obj.selected = true if isSelected("types", obj.id)
- return obj
-
- @rootscope.$broadcast("filters:loaded", @scope.filters)
- return data
+ @rootscope.$broadcast("filters:loaded", @scope.filters)
+ deferred.resolve()
+ return deferred.promise
loadIssues: ->
@scope.urlFilters = @.getUrlFilters()
@@ -263,6 +245,22 @@ class IssuesController extends mixOf(taiga.Controller, taiga.PageMixin, taiga.Fi
.then(=> @.loadFilters())
.then(=> @.loadIssues())
+ saveCurrentFiltersTo: (newFilter) ->
+ deferred = @q.defer()
+ @rs.issues.getMyFilters(@scope.projectId).then (filters) =>
+ filters[newFilter] = @location.search()
+ @rs.issues.storeMyFilters(@scope.projectId, filters).then =>
+ deferred.resolve()
+ return deferred.promise
+
+ deleteMyFilter: (filter) ->
+ deferred = @q.defer()
+ @rs.issues.getMyFilters(@scope.projectId).then (filters) =>
+ delete filters[filter]
+ @rs.issues.storeMyFilters(@scope.projectId, filters).then =>
+ deferred.resolve()
+ return deferred.promise
+
# Functions used from templates
addNewIssue: ->
@rootscope.$broadcast("issueform:new", @scope.project)
@@ -440,29 +438,26 @@ IssuesDirective = ($log, $location) ->
## Issues Filters Directive
#############################################################################
-IssuesFiltersDirective = ($log, $location) ->
+IssuesFiltersDirective = ($log, $location, $rs) ->
template = _.template("""
<% _.each(filters, function(f) { %>
- <% if (f.selected) { %>
-
-
- <%- f.name %>
-
- <%- f.count %>
-
- <% } else { %>
+ <% if (!f.selected) { %>
style="border-left: 3px solid <%- f.color %>;"<% } %>>
<%- f.name %>
+ <% if (f.count){ %>
<%- f.count %>
+ <% } %>
+ <% if (f.type == "myFilters"){ %>
+
+ <% } %>
<% } %>
<% }) %>
+
""")
templateSelected = _.template("""
@@ -501,17 +496,34 @@ IssuesFiltersDirective = ($log, $location) ->
for val in values
selectedFilters.push(val) if val.selected
- renderSelectedFilters()
+ renderSelectedFilters(selectedFilters)
- renderSelectedFilters = ->
+ renderSelectedFilters = (selectedFilters) ->
html = templateSelected({filters:selectedFilters})
$el.find(".filters-applied").html(html)
+ if selectedFilters.length > 0
+ $el.find(".save-filters").show()
+ else
+ $el.find(".save-filters").hide()
renderFilters = (filters) ->
html = template({filters:filters})
$el.find(".filter-list").html(html)
toggleFilterSelection = (type, id) ->
+ if type == "myFilters"
+ $rs.issues.getMyFilters($scope.projectId).then (data) ->
+ myFilters = data
+ filters = myFilters[id]
+ filters.page = 1
+ $ctrl.replaceAllFilters(filters)
+ $ctrl.storeFilters()
+ $ctrl.loadIssues()
+ $ctrl.markSelectedFilters($scope.filters, filters)
+ initializeSelectedFilters($scope.filters)
+ return null
+
+
filters = $scope.filters[type]
filter = _.find(filters, {id:id})
filter.selected = (not filter.selected)
@@ -580,18 +592,57 @@ IssuesFiltersDirective = ($log, $location) ->
$el.on "click", ".filter-list .single-filter", (event) ->
event.preventDefault()
target = angular.element(event.currentTarget)
- if target.hasClass("active")
- target.removeClass("active")
- else
- target.addClass("active")
+ target.toggleClass("active")
id = target.data("id") or null
type = target.data("type")
+
+ # A saved filter can't be active
+ if type == "myFilters"
+ target.removeClass("active")
+
toggleFilterSelection(type, id)
+ $el.on "click", ".filter-list .single-filter .icon-delete", (event) ->
+ event.preventDefault()
+ event.stopPropagation()
+ target = angular.element(event.currentTarget)
+ $ctrl.deleteMyFilter(target.parent().data('id')).then ->
+ $ctrl.loadMyFilters().then (filters) ->
+ $scope.filters.myFilters = filters
+ renderFilters($scope.filters.myFilters)
+
+ $el.on "click", ".save-filters", (event) ->
+ event.preventDefault()
+ renderFilters($scope.filters["myFilters"])
+ showFilters("My filters", "myFilters")
+ $el.find('.save-filters').hide()
+ $el.find('.my-filter-name').show()
+ $el.find('.my-filter-name').focus()
+
+ $el.on "keyup", ".my-filter-name", (event) ->
+ event.preventDefault()
+ if event.keyCode == 13
+ target = angular.element(event.currentTarget)
+ newFilter = target.val()
+ $ctrl.saveCurrentFiltersTo(newFilter).then ->
+ $ctrl.loadMyFilters().then (filters) ->
+ $scope.filters.myFilters = filters
+
+ currentfilterstype = $el.find("h2 a.subfilter span.title").prop('data-type')
+ if currentfilterstype == "myFilters"
+ renderFilters($scope.filters.myFilters)
+
+ $el.find('.my-filter-name').hide()
+ $el.find('.save-filters').show()
+ else if event.keyCode == 27
+ $el.find('.my-filter-name').val('')
+ $el.find('.my-filter-name').hide()
+ $el.find('.save-filters').show()
+
return {link:link}
-module.directive("tgIssuesFilters", ["$log", "$tgLocation", IssuesFiltersDirective])
+module.directive("tgIssuesFilters", ["$log", "$tgLocation", "$tgResources", IssuesFiltersDirective])
module.directive("tgIssues", ["$log", "$tgLocation", IssuesDirective])
diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee
index 60825531..d56d499d 100644
--- a/app/coffee/modules/resources.coffee
+++ b/app/coffee/modules/resources.coffee
@@ -71,6 +71,7 @@ urls = {
"users-change-password-from-recovery": "/api/v1/users/change_password_from_recovery"
"users-change-password": "/api/v1/users/change_password"
"users-change-email": "/api/v1/users/change_email"
+ "user-storage": "/api/v1/user-storage"
"resolver": "/api/v1/resolver"
"userstory-statuses": "/api/v1/userstory-statuses"
"points": "/api/v1/points"
diff --git a/app/coffee/modules/resources/issues.coffee b/app/coffee/modules/resources/issues.coffee
index 013f02c8..696d5757 100644
--- a/app/coffee/modules/resources/issues.coffee
+++ b/app/coffee/modules/resources/issues.coffee
@@ -24,10 +24,11 @@ taiga = @.taiga
generateHash = taiga.generateHash
-resourceProvider = ($repo, $http, $urls, $storage) ->
+resourceProvider = ($repo, $http, $urls, $storage, $q) ->
service = {}
hashSuffix = "issues-queryparams"
filtersHashSuffix = "issues-filters"
+ myFiltersHashSuffix = "issues-my-filters"
service.get = (projectId, issueId) ->
params = service.getQueryParams(projectId)
@@ -79,9 +80,46 @@ resourceProvider = ($repo, $http, $urls, $storage) ->
hash = generateHash([projectSlug, ns])
return $storage.get(hash) or {}
+ service.storeMyFilters = (projectId, myFilters) ->
+ deferred = $q.defer()
+ url = $urls.resolve("user-storage")
+ ns = "#{projectId}:#{myFiltersHashSuffix}"
+ hash = generateHash([projectId, ns])
+ if _.isEmpty(myFilters)
+ promise = $http.delete("#{url}/#{hash}", {key: hash, value:myFilters})
+ promise.then ->
+ deferred.resolve()
+ promise.then null, ->
+ deferred.reject()
+ else
+ promise = $http.put("#{url}/#{hash}", {key: hash, value:myFilters})
+ promise.then (data) ->
+ deferred.resolve()
+ promise.then null, (data) ->
+ innerPromise = $http.post("#{url}", {key: hash, value:myFilters})
+ innerPromise.then ->
+ deferred.resolve()
+ innerPromise.then null, ->
+ deferred.reject()
+ return deferred.promise
+
+ service.getMyFilters = (projectId) ->
+ deferred = $q.defer()
+ url = $urls.resolve("user-storage")
+ ns = "#{projectId}:#{myFiltersHashSuffix}"
+ hash = generateHash([projectId, ns])
+
+ promise = $http.get("#{url}/#{hash}")
+ promise.then (data) ->
+ deferred.resolve(data.data.value)
+ promise.then null, (data) ->
+ deferred.resolve({})
+
+ return deferred.promise
+
return (instance) ->
instance.issues = service
module = angular.module("taigaResources")
-module.factory("$tgIssuesResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", "$tgStorage", resourceProvider])
+module.factory("$tgIssuesResourcesProvider", ["$tgRepo", "$tgHttp", "$tgUrls", "$tgStorage", "$q", resourceProvider])
diff --git a/app/partials/views/modules/issues-filters.jade b/app/partials/views/modules/issues-filters.jade
index 8c0e5b52..b47063f5 100644
--- a/app/partials/views/modules/issues-filters.jade
+++ b/app/partials/views/modules/issues-filters.jade
@@ -8,6 +8,7 @@ section.filters
a.icon.icon-search(href="", title="search")
div.filters-step-cat
div.filters-applied
+ a.hidden.button.save-filters(href="", title="save", ng-class="{hidden: filters.length}") save
h2
a.hidden.subfilter(href="", title="cat-name")
span.title status
@@ -41,5 +42,9 @@ section.filters
a(href="", title="Created by", data-type="createdBy")
span.title Created by
span.icon.icon-arrow-right
+ li
+ a(href="", title="My filters", data-type="myFilters")
+ span.title My filters
+ span.icon.icon-arrow-right
div.filter-list.hidden
diff --git a/app/styles/modules/filters/filters.scss b/app/styles/modules/filters/filters.scss
index bd90074c..7297389f 100644
--- a/app/styles/modules/filters/filters.scss
+++ b/app/styles/modules/filters/filters.scss
@@ -61,6 +61,21 @@
}
}
+.filters-step-cat {
+ .save-filters {
+ float: right;
+ padding: 8px 0px;
+ }
+ .my-filter-name {
+ background: $grayer;
+ color: $whitish;
+ width: 100%;
+ @include placeholder {
+ color: $gray-light;
+ }
+ }
+}
+
.filters-cats {
margin-top: 2rem;
li {