diff --git a/app/assets/javascripts/app/controllers/_application_controller.coffee b/app/assets/javascripts/app/controllers/_application_controller.coffee index f810ad823..268a38505 100644 --- a/app/assets/javascripts/app/controllers/_application_controller.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller.coffee @@ -487,25 +487,28 @@ class App.Controller extends Spine.Controller prepareForObjectList: (items) -> for item in items - - item.link = '' - item.title = '???' - - # convert backend name space to local name space - item.object = item.object.replace('::', '') - - # lookup real data - if App[item.object] && App[item.object].exists(item.o_id) - object = App[item.object].find(item.o_id) - item.objectNative = object - item.link = object.uiUrl() - item.title = object.displayName() - item.object_name = object.objectDisplayName() - item.cssIcon = object.iconActivity(@Session.get()) - - item.created_by = App.User.find(item.created_by_id) + item = @prepareForObjectListItem(item) items + prepareForObjectListItem: (item) -> + item.link = '' + item.title = '???' + + # convert backend name space to local name space + item.object = item.object.replace('::', '') + + # lookup real data + if App[item.object] && App[item.object].exists(item.o_id) + object = App[item.object].find(item.o_id) + item.objectNative = object + item.link = object.uiUrl() + item.title = object.displayName() + item.object_name = object.objectDisplayName() + item.cssIcon = object.iconActivity(@Session.get()) + + item.created_by = App.User.find(item.created_by_id) + item + # central method, is getting called on every ticket form change ticketFormChanges: (params, attribute, attributes, classname, form, ui) => if @formMeta.dependencies && @formMeta.dependencies[attribute.name] diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee index 08bda0a79..a2551a2b1 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee @@ -740,3 +740,311 @@ class App.WizardFullScreen extends App.WizardModal super $('.content').addClass('hide') $('#content').removeClass('hide') + +class App.CollectionController extends App.Controller + events: + 'click .js-remove': 'remove' + 'click .js-item': 'click' + 'click .js-locationVerify': 'location' + observe: + field1: true + field2: false + #currentItems: {} + #1: + # a: 123 + # b: 'some string' + #2: + # a: 123 + # b: 'some string' + #renderList: {} + #1: ..dom..ref.. + #2: ..dom..ref.. + template: '_need_to_be_defined_' + uniqKey: 'id' + model: '_need_to_be_defined_' + sortBy: 'name' + order: 'ASC', + + constructor: -> + @events = @constructor.events unless @events + @observe = @constructor.observe unless @observe + @currentItems = {} + @renderList = {} + @queue = [] + @queueRunning = false + @lastOrder = [] + + super + + @queue.push ['renderAll'] + @uIRunner() + + # bind to changes + if @model + @subscribeId = App[@model].subscribe(@collectionSync) + + # render on generic ui call + @bind('ui:rerender', => + @queue.push ['renderAll'] + @uIRunner() + ) + + # render on login + @bind('auth:login', => + @queue.push ['renderAll'] + @uIRunner() + ) + + # reset current tasks on logout + @bind('auth:logout', => + @queue.push ['renderAll'] + @uIRunner() + ) + + @log 'debug', 'Init @uniqKey', @uniqKey + @log 'debug', 'Init @observe', @observe + @log 'debug', 'Init @model', @model + + release: => + if @subscribeId + App[@model].unsubscribe(@subscribeId) + + uIRunner: -> + return if !@queue[0] + return if @queueRunning + @queueRunning = true + loop + param = @queue.shift() + if param[0] is 'domChange' + @domChange(param[1]) + else if param[0] is 'domRemove' + @domRemove(param[1]) + else if param[0] is 'renderAll' + @renderAll() + if !@queue[0] + @onRenderEnd() + @queueRunning = false + break + + collectionOrderGet: => + newOrder = [] + all = @itemsAll() + for item in all + newOrder.push item[@uniqKey] + newOrder + + collectionOrderSet: (newOrder = false) => + if !newOrder + newOrder = @collectionOrderGet() + @lastOrder = newOrder + + collectionSync: (items, type) => + console.log('collectionSync', items, type) + + # remove items + if type is 'destroy' + ids = [] + for item in items + ids.push item.id + @queue.push ['domRemove', ids] + @uIRunner() + return + + # inital render + if _.isEmpty(@renderList) + @queue.push ['renderAll'] + @uIRunner() + return + + # check if item order is the same + newOrder = @collectionOrderGet() + removedIds = _.difference(@lastOrder, newOrder) + addedIds = _.difference(newOrder, @lastOrder) + + @log 'debug', 'collectionSync removedIds', removedIds + @log 'debug', 'collectionSync addedIds', addedIds + @log 'debug', 'collectionSync @lastOrder', @lastOrder + @log 'debug', 'collectionSync newOrder', newOrder + + # add items + alreadyRemoved = false + if !_.isEmpty(addedIds) + lastOrderNew = [] + for id in @lastOrder + if !_.contains(removedIds, id) + lastOrderNew.push id + + # try to find positions of new items + applyOrder = App.Utils.diffPositionAdd(lastOrderNew, newOrder) + if !applyOrder + @queue.push ['renderAll'] + @uIRunner() + return + + if !_.isEmpty(removedIds) + alreadyRemoved = true + @queue.push ['domRemove', removedIds] + @uIRunner() + + newItems = [] + for apply in applyOrder + item = App[@model].find(apply.id) + item.meta_position = apply.position + newItems.push item + @queue.push ['domChange', newItems] + @uIRunner() + + # remove items + if !alreadyRemoved && !_.isEmpty(removedIds) + @queue.push ['domRemove', removedIds] + @uIRunner() + + # update items + newItems = [] + for item in items + if !_.contains(removedIds, item.id) && !_.contains(addedIds, item.id) + newItems.push item + return if _.isEmpty(newItems) + @queue.push ['domChange', newItems] + @uIRunner() + #return + + # rerender all items + #@queue.push ['renderAll'] + #@uIRunner() + + domRemove: (ids) => + @log 'debug', 'domRemove', ids + for id in ids + @itemAttributesDelete(id) + if @renderList[id] + @renderList[id].remove() + delete @renderList[id] + @onRemoved(id) + @collectionOrderSet() + + domChange: (items) => + @log 'debug', 'domChange items', items + @log 'debug', 'domChange @currentItems', @currentItems + changedItems = [] + for item in items + @log 'debug', 'domChange|item', item + attributes = @itemAttributes(item) + currentItem = @itemAttributesGet(item[@uniqKey]) + if !currentItem + @log 'debug', 'domChange|add', item + changedItems.push item + @itemAttributesSet(item[@uniqKey], attributes) + else + @log 'debug', 'domChange|change', item + @log 'debug', 'domChange|change|observe attributes', @observe + @log 'debug', 'domChange|change|current', currentItem + @log 'debug', 'domChange|change|new', attributes + for field of @observe + @log 'debug', 'domChange|change|compare', field, currentItem[field], attributes[field] + diff = !_.isEqual(currentItem[field], attributes[field]) + @log 'debug', 'domChange|diff', diff, item + if diff + changedItems.push item + @itemAttributesSet(item[@uniqKey], attributes) + break + return if _.isEmpty(changedItems) + @renderParts(changedItems) + + renderAll: => + items = @itemsAll() + @log 'debug', 'renderAll', items + localeEls = [] + for item in items + attributes = @itemAttributes(item) + @itemAttributesSet(item[@uniqKey], attributes) + localeEls.push @renderItem(item, false) + @html localeEls + @collectionOrderSet() + @onRenderEnd() + + itemsAll: => + #App[@model].all() + App[@model].search(sortBy: @sortBy, order: @order) + + itemAttributesDiff: (item) => + attributes = @itemAttributes(item) + currentItem = @itemAttributesGet(item[@uniqKey]) + for field of @observe + @log 'debug', 'itemAttributesDiff|compare', field, currentItem[field], attributes[field] + diff = !_.isEqual(currentItem[field], attributes[field]) + if diff + @log 'debug', 'itemAttributesDiff|diff', diff + return true + false + + itemAttributesDelete: (id) => + delete @currentItems[id] + + itemAttributesGet: (id) => + @currentItems[id] + + itemAttributesSet: (id, attributes) => + @currentItems[id] = attributes + + itemAttributes: (item) => + attributes = {} + for field of @observe + attributes[field] = item[field] + attributes + + renderParts: (items) => + @log 'debug', 'renderParts', items + for item in items + if !@renderList[item[@uniqKey]] + @renderItem(item) + else + @renderItem(item, @renderList[item[@uniqKey]]) + @collectionOrderSet() + + renderItem: (item, el) => + if @prepareForObjectListItemSupport + item = @prepareForObjectListItem(item) + @log 'debug', 'renderItem', item, @template, el, @renderList[item[@uniqKey]] + html = $(App.view(@template)( + item: item + )) + @renderList[item[@uniqKey]] = html + if el is false + return html + else if !el + position = item.meta_position + 1 + console.log('!el', item, position, item.meta_position) + #if item.meta_position + @el.find(".js-item:nth-child(#{position})").before(html) + else + el.replaceWith(html) + + onRenderEnd: -> + # nothing + + location: (e) -> + @locationVerify(e) + + click: (e) => + row = $(e.target).closest('.js-item') + id = row.data('id') + console.log('click id', id) + @onClick(id, e) + + onClick: (id, e) -> + # nothing + + remove: (e) => + e.preventDefault() + e.stopPropagation() + row = $(e.target).closest('.js-item') + id = row.data('id') + @onRemove(id,e) + App[@model].destroy(id) + + onRemove: (id, e) -> + # nothing + + onRemoved: (id) -> + # nothing diff --git a/app/assets/javascripts/app/controllers/taskbar_widget.coffee b/app/assets/javascripts/app/controllers/taskbar_widget.coffee index b54a8a85c..c891f9e65 100644 --- a/app/assets/javascripts/app/controllers/taskbar_widget.coffee +++ b/app/assets/javascripts/app/controllers/taskbar_widget.coffee @@ -1,37 +1,19 @@ -class App.TaskbarWidget extends App.Controller +class App.TaskbarWidget extends App.CollectionController events: 'click .js-close': 'remove' 'click .js-locationVerify': 'location' - fields: - observe: - field1: true - field2: false - meta: true - active: true - notify: true - observeNot: - field1: true - field2: false - currentItems: {} - #1: - # a: 123 - # b: 'some string' - #2: - # a: 123 - # b: 'some string' - renderList: {} - #1: ..dom..ref.. - #2: ..dom..ref.. + + model: false template: 'widget/task_item' + uniqKey: 'key' + observe: + meta: true + active: true + notify: true constructor: -> super - @queue = [] - @queueRunning = false - - @renderAll() - dndOptions = tolerance: 'pointer' distance: 15 @@ -51,108 +33,24 @@ class App.TaskbarWidget extends App.Controller @el.sortable(dndOptions) # bind to changes - @bind('taskInit', => @renderAll()) + @bind('taskInit', => + console.log('renderAll aaa') + @renderAll() + console.log('renderAll bbb') + #@queue.push ['renderAll'] + #@uIRunner() + ) @bind('taskUpdate', (tasks) => - @queue.push ['taskUpdate', tasks] + @queue.push ['domChange', tasks] @uIRunner() ) @bind('taskRemove', (task_ids) => - @queue.push ['checkRemoves', task_ids] + @queue.push ['domRemove', task_ids] @uIRunner() ) - # render on generic ui call - @bind('ui:rerender', => - @queue.push ['renderAll'] - @uIRunner() - ) - - # render on login - @bind('auth:login', => - @queue.push ['renderAll'] - @uIRunner() - ) - - # reset current tasks on logout - @bind('auth:logout', => - @queue.push ['renderAll'] - @uIRunner() - ) - - uIRunner: -> - return if !@queue[0] - return if @queueRunning - @queueRunning = true - loop - param = @queue.shift() - if param[0] is 'taskUpdate' - @checkChanges(param[1]) - else if param[0] is 'checkRemoves' - @checkRemoves(param[1]) - else if param[0] is 'renderAll' - @renderAll() - if !@queue[0] - @queueRunning = false - break - - checkRemoves: (keys) -> - for key in keys - delete @currentItems[key] - if @renderList[key] - @renderList[key].remove() - delete @renderList[key] - - checkChanges: (items) -> - changedItems = [] - for item in items - attributes = {} - for field of @fields.observe - attributes[field] = item[field] - #console.log('item', item) - #attributes = item.attributes() - #console.log('item', @fields.observe, item, attributes) - if !@currentItems[item.key] - changedItems.push item - @currentItems[item.key] = attributes - else - currentItem = @currentItems[item.key] - hit = false - for field of @fields.observe - diff = _.isEqual(currentItem[field], attributes[field]) - #console.log('diff', field, diff, currentItem[field], attributes[field]) - if !hit && diff - changedItems.push item - @currentItems[item.key] = attributes - hit = true - return if _.isEmpty(changedItems) - @renderParts(changedItems) - - renderAll: -> - #@html '' - items = App.TaskManager.allWithMeta() - localeEls = [] - for item in items - localeEls.push @renderItem(item, false) - @html localeEls - - renderParts: (items) -> - for item in items - if !@renderList[item.key] - @renderItem(item) - else - @renderItem(item, @renderList[item.key]) - - renderItem: (item, el) -> - html = $(App.view(@template)( - item: item - )) - @renderList[item.key] = html - if el is false - return html - else if !el - @el.append(html) - else - el.replaceWith(html) + itemsAll: -> + App.TaskManager.allWithMeta() location: (e) => return if !$(e.currentTarget).hasClass('is-modified') diff --git a/app/assets/javascripts/app/controllers/widget/online_notification.coffee b/app/assets/javascripts/app/controllers/widget/online_notification.coffee index 71642dffc..1cacc0ff7 100644 --- a/app/assets/javascripts/app/controllers/widget/online_notification.coffee +++ b/app/assets/javascripts/app/controllers/widget/online_notification.coffee @@ -7,13 +7,11 @@ class App.OnlineNotificationWidget extends App.Controller events: 'click .js-mark': 'markAllAsRead' - 'click .js-item': 'hide' - 'click .js-remove': 'removeItem' - 'click .js-locationVerify': 'onItemClick' 'click': 'stopPropagation' elements: '.js-mark': 'mark' + '.js-noNotifications': 'noNotifications' '.js-item': 'item' '.js-content': 'content' '.js-header': 'header' @@ -25,7 +23,7 @@ class App.OnlineNotificationWidget extends App.Controller @bind 'OnlineNotification::changed', => @delay( => @fetch() - 2600 + 2200 'online-notification-changed' ) @@ -48,20 +46,10 @@ class App.OnlineNotificationWidget extends App.Controller if !@access() @counterUpdate(0) return - if !@subscribeId - @subscribeId = App.OnlineNotification.subscribe(@updateContent) - - if @access() - @subscribeId = App.OnlineNotification.subscribe(@updateContent) - - @bind('ui:reshow', => - @show() - 'popover' - ) $(window).on 'click.notifications', @hide - @updateContent() + @createContainer() release: -> $(window).off 'click.notifications' @@ -84,14 +72,15 @@ class App.OnlineNotificationWidget extends App.Controller @nudge(e, 1) return else if e.keyCode is 13 # enter - @item.filter('.is-hover').find('.js-locationVerify').click() + @$('.js-item').filter('.is-hover').find('.js-locationVerify').click() nudge: (e, position) -> # get current - current = @item.filter('.is-hover') + items = @$('.js-item') + current = items.filter('.is-hover') if !current.size() - @item.first().addClass('is-hover') + items.first().addClass('is-hover') return if position is 1 @@ -110,27 +99,41 @@ class App.OnlineNotificationWidget extends App.Controller if prev @scrollToIfNeeded(prev, true) - counterUpdate: (count) => + counterUpdate: (count, force = false) => count = '' if count is 0 + return if !force && @count is count + @count = count + $('.js-notificationsCounter').text(count) App.Event.trigger('online_notification_counter', count.toString()) - @count = count - # show mark all as read if needed if !count - @mark.addClass('hidden') + @mark.addClass('hide') else - @mark.removeClass('hidden') + @mark.removeClass('hide') - markAllAsRead: (event) -> - event.preventDefault() + counterGen: (force = false) => + items = App.OnlineNotification.all() + count = 0 + for item in items + if !item.seen + count++ + @counterUpdate(count, force) + + if _.isEmpty(items) + @noNotifications.removeClass('hide') + else + @noNotifications.addClass('hide') + + markAllAsRead: (e) -> + e.preventDefault() @counterUpdate(0) @ajax( id: 'markAllAsRead' type: 'POST' - url: @apiPath + '/online_notifications/mark_all_as_read' + url: "#{@apiPath}/online_notifications/mark_all_as_read" data: JSON.stringify('') processData: true ) @@ -143,13 +146,14 @@ class App.OnlineNotificationWidget extends App.Controller heightPopoverHeader = @header.outerHeight(true) isOverflowing = false heightPopoverContent = 0 - @item.each (i, el) => + @$('.js-item').each (i, el) => # accumulate height of items heightPopoverContent += el.clientHeight if (heightPopoverHeader + heightPopoverContent + heightPopoverSpacer) > heightApp - @content.css 'height', heightApp - heightPopoverHeader - heightPopoverSpacer + containerHeight = heightApp - heightPopoverHeader - heightPopoverSpacer + @content.css 'height', containerHeight isOverflowing = true return false # exit .each loop @@ -160,7 +164,6 @@ class App.OnlineNotificationWidget extends App.Controller load = (data) => @fetchedData = true App.OnlineNotification.refresh(data.stream, clear: true) - @updateContent() App.OnlineNotification.fetchFull(load) toggle: => @@ -169,43 +172,23 @@ class App.OnlineNotificationWidget extends App.Controller else @show() - updateContent: => + createContainer: => if !@Session.get() @content.html('') return - items = App.OnlineNotification.search(sortBy: 'created_at', order: 'DESC') - @count = 0 - for item in items - if !item.seen - @count++ + count = '' + localeEl = $( App.view('widget/online_notification')( + count: count + )) - @counterUpdate(@count) - - # update content - items = @prepareForObjectList(items) - - # generate desktop notifications - for item in items - if !@alreadyShown[item.id] - @alreadyShown[item.id] = true - if @fetchedData - if item.objectNative && item.objectNative.activityMessage - title = item.objectNative.activityMessage(item) - else - title = "Need objectNative in item #{item.object}.find(#{item.o_id})" - title = App.Utils.html2text(title.replace(/<.+?>/g, '"')) - @notifyDesktop( - url: item.link - title: title - ) - App.OnlineNotification.play() - - @html App.view('widget/online_notification')( - items: items - count: @count + new App.OnlineNotificationContentWidget( + el: localeEl.find('.js-items') + container: @ ) + @html localeEl + @counterGen(true) return if !@shown @show() @@ -221,20 +204,43 @@ class App.OnlineNotificationWidget extends App.Controller @shown = false @el.hide() - onItemClick: (e) -> - @locationVerify(e) - @hide() - stopPropagation: (e) -> e.stopPropagation() - removeItem: (e) => - e.preventDefault() - e.stopPropagation() - row = $(e.target).closest('.js-item') - id = row.data('id') - App.OnlineNotification.destroy(id) - @updateHeight() - remove: => - @el.remove() \ No newline at end of file + @el.remove() + +class App.OnlineNotificationContentWidget extends App.CollectionController + model: 'OnlineNotification' + template: 'widget/online_notification_item' + prepareForObjectListItemSupport: true + observe: + seen: true + sortBy: 'created_at' + order: 'DESC' + alreadyShown: {} + + onRenderEnd: -> + @container.counterGen() + @container.updateHeight() + + # generate desktop notifications + items = App.OnlineNotification.search(sortBy: 'created_at', order: 'DESC') + for item in items + if !@alreadyShown[item.id] + @alreadyShown[item.id] = true + if @container.fetchedData + item = @prepareForObjectListItem(item) + if item.objectNative && item.objectNative.activityMessage + title = item.objectNative.activityMessage(item) + else + title = "Need objectNative in item #{item.object}.find(#{item.o_id})" + title = App.Utils.html2text(title.replace(/<.+?>/g, '"')) + @notifyDesktop( + url: item.link + title: title + ) + App.OnlineNotification.play() + + onClick: -> + @container.hide() diff --git a/app/assets/javascripts/app/lib/app_post/collection.coffee b/app/assets/javascripts/app/lib/app_post/collection.coffee index 7c7fb635b..cb99b3832 100644 --- a/app/assets/javascripts/app/lib/app_post/collection.coffee +++ b/app/assets/javascripts/app/lib/app_post/collection.coffee @@ -61,7 +61,7 @@ class _collectionSingleton extends Spine.Module return # reset in-memory - appObject.refresh(params.data, { clear: true }) + appObject.refresh(params.data, clear: true) loadAssets: (assets) -> @log 'debug', 'loadAssets', assets diff --git a/app/assets/javascripts/app/lib/app_post/utils.coffee b/app/assets/javascripts/app/lib/app_post/utils.coffee index 544ebb9e3..c8f78ab6e 100644 --- a/app/assets/javascripts/app/lib/app_post/utils.coffee +++ b/app/assets/javascripts/app/lib/app_post/utils.coffee @@ -626,3 +626,26 @@ class App.Utils $outer.remove() return 100 - widthWithScroll + + @diffPositionAdd: (a, b) -> + applyOrder = [] + newOrderMethod = (a, b, applyOrder) -> + for position of b + if a[position] isnt b[position] + + # changes to complex, whole rerender + if _.contains(a, b[position]) + return false + + # insert new item and try next + a.splice(position, 0, b[position]) + apply = + position: parseInt(position) + id: b[position] + applyOrder.push apply + newOrderMethod(a, b, applyOrder) + true + + result = newOrderMethod(a, b, applyOrder) + return false if !result + applyOrder diff --git a/app/assets/javascripts/app/models/_application_model.coffee b/app/assets/javascripts/app/models/_application_model.coffee index 8acd0b8f7..eff251998 100644 --- a/app/assets/javascripts/app/models/_application_model.coffee +++ b/app/assets/javascripts/app/models/_application_model.coffee @@ -309,11 +309,33 @@ class App.Model extends Spine.Model # subscribe and render data / fetch new data if triggered @bind( - 'refresh change remove' - (items) => - App.Log.debug('Model', "local collection refresh/change #{@className}", items) + 'refresh' + (items, options) => + if !_.isArray(items) + items = [items] + console.log('refresh', items, options) + App.Log.debug('Model', "local collection refresh #{@className}", items) for key, callback of @SUBSCRIPTION_COLLECTION - callback(items) + callback(items, 'refresh') + ) + @bind( + 'change' + (items, subEvent) => + return if subEvent is 'destroy' + if !_.isArray(items) + items = [items] + App.Log.debug('Model', "local collection change #{@className}", items) + for key, callback of @SUBSCRIPTION_COLLECTION + callback(items, 'change') + ) + @bind( + 'destroy' + (items) => + if !_.isArray(items) + items = [items] + App.Log.debug('Model', "local collection destroy #{@className}", items) + for key, callback of @SUBSCRIPTION_COLLECTION + callback(items, 'destroy') ) # fetch() all on network notify diff --git a/app/assets/javascripts/app/views/widget/online_notification.jst.eco b/app/assets/javascripts/app/views/widget/online_notification.jst.eco index da34fb6d3..c0576023f 100644 --- a/app/assets/javascripts/app/views/widget/online_notification.jst.eco +++ b/app/assets/javascripts/app/views/widget/online_notification.jst.eco @@ -1,37 +1,11 @@