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 @@
<%- @T('Notifications') %> <%- @count %>
- <%- @T( 'Mark all as read' ) %> + <%- @T( 'Mark all as read' ) %>
- <% if @items.length: %> - <% for item in @items: %> -
- href="<%- item.created_by.uiUrl() %>"<% end %>> - <%- item.created_by.avatar() %> - - -
- <% end %> - <% else: %> -
- <%- @T("No unread Notifications for you. :) ") %> -
- <% end %> +
+ <%- @T("No unread Notifications for you. :) ") %> +
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/widget/online_notification_content.jst.eco b/app/assets/javascripts/app/views/widget/online_notification_content.jst.eco deleted file mode 100644 index 12605f565..000000000 --- a/app/assets/javascripts/app/views/widget/online_notification_content.jst.eco +++ /dev/null @@ -1,30 +0,0 @@ -<% if @items.length: %> - <% for item in @items: %> -
- href="<%- item.created_by.uiUrl() %>"<% end %>> - <%- item.created_by.avatar() %> - -
- - - <% if item.objectNative && item.objectNative.activityMessage: %> - <%- item.objectNative.activityMessage(item) %> - <% else: %> - Need objectNative in item <%= item.object %>.find(<%= item.o_id %>) - <% end %> - - <%- @humanTime(item.created_at, false, 'activity-time') %> - -
-
- <%- @Icon('diagonal-cross') %> -
-
-
-
- <% end %> -<% else: %> -
- <%- @T("No unread Notifications for you. :) ") %> -
-<% end %> \ No newline at end of file diff --git a/app/assets/javascripts/app/views/widget/online_notification_item.jst.eco b/app/assets/javascripts/app/views/widget/online_notification_item.jst.eco new file mode 100644 index 000000000..ac1da0bd2 --- /dev/null +++ b/app/assets/javascripts/app/views/widget/online_notification_item.jst.eco @@ -0,0 +1,22 @@ +
+ href="<%- @item.created_by.uiUrl() %>"<% end %>> + <%- @item.created_by.avatar() %> + +
+ + + <% if @item.objectNative && @item.objectNative.activityMessage: %> + <%- @item.objectNative.activityMessage(@item) %> + <% else: %> + Need objectNative in item <%= @item.object %>.find(<%= @item.o_id %>) + <% end %> + + <%- @humanTime(@item.created_at, false, 'activity-time') %> + +
+
+ <%- @Icon('diagonal-cross') %> +
+
+
+
diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index e57f5b471..2baa5cb62 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -3730,6 +3730,7 @@ footer { padding-left: 0; padding-right: 0; overflow-y: auto; + margin-bottom: 10px; } &.is-overflowing .popover-notificationsHeader { diff --git a/public/assets/tests/html_utils.js b/public/assets/tests/html_utils.js index 85f3afb7e..ea9349176 100644 --- a/public/assets/tests/html_utils.js +++ b/public/assets/tests/html_utils.js @@ -1400,4 +1400,74 @@ test("check formatTime format", function() { equal(verify, result, string) }); +// check diffPosition +test("check diffPosition format", function() { + + var a = [1,2,3,4] + var b = [1,2,3,4,5] + var result = [ + { + position: 4, + id: 5, + }, + ] + var verify = App.Utils.diffPositionAdd(a, b) + deepEqual(verify, result) + + a = [2,3,4] + b = [1,2,3,4] + result = [ + { + position: 0, + id: 1, + }, + ] + verify = App.Utils.diffPositionAdd(a, b) + deepEqual(verify, result) + + a = [2,3,4] + b = [1,2,3,4,5] + result = [ + { + position: 0, + id: 1, + }, + { + position: 4, + id: 5, + }, + ] + verify = App.Utils.diffPositionAdd(a, b) + deepEqual(verify, result) + + a = [2,3,4] + b = [1,99,12,2,3,4,5] + result = [ + { + position: 0, + id: 1, + }, + { + position: 1, + id: 99, + }, + { + position: 2, + id: 12, + }, + { + position: 6, + id: 5, + }, + ] + verify = App.Utils.diffPositionAdd(a, b) + deepEqual(verify, result) + + a = [4,3,1] + b = [1,2,3,4,5] + result = false + verify = App.Utils.diffPositionAdd(a, b) + deepEqual(verify, result) +}); + } \ No newline at end of file diff --git a/test/browser/agent_ticket_actions_level9_test.rb b/test/browser/agent_ticket_actions_level9_test.rb new file mode 100644 index 000000000..e2c0f752c --- /dev/null +++ b/test/browser/agent_ticket_actions_level9_test.rb @@ -0,0 +1,393 @@ +# encoding: utf-8 +require 'browser_test_helper' + +class AgentTicketActionLevel9Test < TestCase + + def test_online_notifications + + @browser = browser_instance + login( + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all() + + # create new ticket + ticket1 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification #1', + body: 'online notification #1', + }, + ) + + browser2 = browser_instance + login( + browser: browser2, + username: 'agent1@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all(browser: browser2) + + click( + browser: browser2, + css: '.js-toggleNotifications', + ) + click( + browser: browser2, + css: '.js-mark', + ) + sleep 2 + + # remove all notificatons + online_notitifcation_close_all( + browser: browser2, + ) + + exists_not( + browser: browser2, + css: '.js-noNotifications.hide', + ) + match( + browser: browser2, + css: '.js-noNotifications', + value: 'No unread Notifications', + ) + exists( + browser: browser2, + css: '.js-mark.hide', + ) + match_not( + browser: browser2, + css: '.js-notificationsCounter', + value: '\d', + no_quote: true, + ) + + ticket2 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification #2', + body: 'online notification #2', + }, + ) + + watch_for( + browser: browser2, + css: '.js-notificationsContainer .js-item', + value: 'online notification #2', + timeout: 10, + ) + match( + browser: browser2, + css: '.js-notificationsCounter', + value: '1', + ) + + exists_not( + browser: browser2, + css: '.js-mark.hide', + ) + + ticket3 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification #3', + body: 'online notification #3', + }, + ) + + watch_for( + browser: browser2, + css: '.js-notificationsContainer .js-item', + value: 'online notification #3', + timeout: 6, + ) + watch_for( + browser: browser2, + css: '.js-notificationsCounter', + value: '2', + ) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item') + count = items.count + assert_equal(2, count) + + items[1].click + + click( + browser: browser2, + css: '.js-toggleNotifications', + ) + + watch_for( + browser: browser2, + css: '.js-notificationsCounter', + value: '1', + ) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item') + assert_equal(2, items.count) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item.is-inactive') + assert_equal(1, items.count) + + ticket4 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification #4', + body: 'online notification #4', + }, + ) + + watch_for( + browser: browser2, + css: '.js-notificationsCounter', + value: '2', + ) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item') + assert_equal(3, items.count) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item.is-inactive') + assert_equal(1, items.count) + + click( + browser: browser2, + css: '.js-mark', + ) + sleep 3 + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item') + assert_equal(3, items.count) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item.is-inactive') + assert_equal(3, items.count) + + match_not( + browser: browser2, + css: '.js-notificationsCounter', + value: '\d', + no_quote: true, + ) + + ticket5 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification #5', + body: 'online notification #5', + }, + ) + + watch_for( + browser: browser2, + css: '.js-notificationsCounter', + value: '1', + ) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item') + assert_equal(4, items.count) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item.is-inactive') + assert_equal(3, items.count) + + # Note: title update will generate extra notification - so we will have 5 + ticket_update( + data: { + title: 'online notification #5/5', + state: 'closed', + }, + ) + + watch_for( + browser: browser2, + css: '.js-notificationsContainer .js-item', + value: 'online notification #5/5', + timeout: 20, + ) + + watch_for( + browser: browser2, + css: '.js-notificationsContainer .js-item.is-inactive', + value: 'online notification #5/5', + timeout: 20, + ) + + match_not( + browser: browser2, + css: '.js-notificationsCounter', + value: '\d', + no_quote: true, + ) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item') + assert_equal(6, items.count) + + items = browser2.find_elements(css: '.js-notificationsContainer .js-item.is-inactive') + assert_equal(6, items.count) + + end + + def test_online_notifications_render + @browser = browser_instance + login( + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all() + + browser2 = browser_instance + login( + browser: browser2, + username: 'agent1@example.com', + password: 'test', + url: browser_url, + ) + tasks_close_all(browser: browser2) + click( + browser: browser2, + css: '.js-toggleNotifications', + ) + online_notitifcation_close_all(browser: browser2) + + ticket1 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification render #1', + body: 'online notification render #1', + }, + ) + ticket2 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification render #2', + body: 'online notification render #2', + }, + ) + + watch_for( + browser: browser2, + css: '.js-notificationsCounter', + value: '2', + ) + + execute( + browser: browser2, + js: '$(".js-notificationsContainer .js-items .js-item:nth-child(1) .activity-text").text("render test 2")', + ) + execute( + browser: browser2, + js: '$(".js-notificationsContainer .js-items .js-item:nth-child(2) .activity-text").text("render test 1")', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(1) .activity-text', + value: 'render test 2', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(2) .activity-text', + value: 'render test 1', + ) + ticket3 = ticket_create( + data: { + customer: 'nico', + group: 'Users', + title: 'online notification render #3', + body: 'online notification render #3', + }, + ) + watch_for( + browser: browser2, + css: '.js-notificationsCounter', + value: '3', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(1) .activity-text', + value: 'online notification render #3', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(2) .activity-text', + value: 'render test 2', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(3) .activity-text', + value: 'render test 1', + ) + + ticket_update( + data: { + state: 'closed', + }, + ) + watch_for( + browser: browser2, + css: '.js-notificationsCounter', + value: '2', + ) + + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .is-inactive.js-item:nth-child(1) .activity-text', + value: 'online notification render #3', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .is-inactive.js-item:nth-child(2) .activity-text', + value: 'online notification render #3', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(3) .activity-text', + value: 'render test 2', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(4) .activity-text', + value: 'render test 1', + ) + + execute( + browser: browser2, + js: '$(".js-notificationsContainer .js-items .js-item:nth-child(2) .activity-text").text("render test 3")', + ) + + close_online_notitifcation( + browser: browser2, + data: { + position: 3, + }, + ) + + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .is-inactive.js-item:nth-child(1) .activity-text', + value: 'online notification render #3', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(2) .activity-text', + value: 'render test 3', + ) + match( + browser: browser2, + css: '.js-notificationsContainer .js-items .js-item:nth-child(3) .activity-text', + value: 'render test 1', + ) + + end + +end diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index 26a5c9883..90342e0a0 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -186,7 +186,7 @@ class TestCase < Test::Unit::TestCase end instance.find_elements(css: '#login button')[0].click - sleep 5 + sleep 4 login = instance.find_elements(css: '.user-menu .user a')[0].attribute('title') if login != params[:username] screenshot(browser: instance, comment: 'login_failed') @@ -264,7 +264,7 @@ class TestCase < Test::Unit::TestCase return if !clues instance.execute_script("$('.js-modal--clue .js-close').click()") assert(true, 'clues closed') - sleep 2 + sleep 1 end =begin @@ -1222,7 +1222,7 @@ wait untill text in selector disabppears instance = params[:browser] || @browser (1..100).each do - sleep 0.1 + #sleep 0.5 begin if instance.find_elements(css: '.navigation .tasks .task:first-child')[0] instance.mouse.move_to(instance.find_elements(css: '.navigation .tasks .task:first-child')[0]) @@ -1247,6 +1247,85 @@ wait untill text in selector disabppears assert(true, 'all tasks closed') end +=begin + + close_online_notitifcation( + browser: browser1, + data: { + #title: 'some title', + position: 3, + }, + ) + +=end + + def close_online_notitifcation(params = {}) + switch_window_focus(params) + log('close_online_notitifcation', params) + + instance = params[:browser] || @browser + data = params[:data] + + if data[:title] + element = instance.find_elements(partial_link_text: data[:title])[0] + if !element + screenshot(browser: instance, comment: 'close_online_notitifcation') + raise "no online notification with title '#{data[:title]}' found" + end + instance.mouse.move_to(element) + sleep 0.1 + instance.execute_script("$('.js-notificationsContainer .js-items .js-item .activity-text:contains(\"#{data[:title]}\") .js-remove').first().click()") + + else + css = ".js-notificationsContainer .js-items .js-item:nth-child(#{data[:position]})" + element = instance.find_elements(css: css)[0] + if !element + screenshot(browser: instance, comment: 'close_online_notitifcation') + raise "no online notification with postion '#{css}' found" + end + + instance.mouse.move_to(element) + sleep 0.1 + instance.find_elements(css: "#{css} .js-remove")[0].click + end + + true + end + +=begin + + online_notitifcation_close_all( + browser: browser1, + ) + +=end + + def online_notitifcation_close_all(params = {}) + switch_window_focus(params) + log('online_notitifcation_close_all', params) + + instance = params[:browser] || @browser + + (1..100).each do + sleep 0.5 + begin + if instance.find_elements(css: '.js-notificationsContainer .js-item:first-child')[0] + instance.mouse.move_to(instance.find_elements(css: '.js-notificationsContainer .js-item:first-child')[0]) + sleep 0.1 + click_element = instance.find_elements(css: '.js-notificationsContainer .js-item:first-child .js-remove')[0] + if click_element + click_element.click + end + else + break + end + rescue + # try again + end + end + assert(true, 'all online notification closed') + end + =begin empty_search( @@ -1525,6 +1604,7 @@ wait untill text in selector disabppears customer: 'nico', group: 'Users', # optional / '-NONE-' # if group selection should not be shown priority: '2 normal', + state: 'open', title: 'overview #1', body: 'overview #1', }, @@ -1598,6 +1678,14 @@ wait untill text in selector disabppears mute_log: true, ) end + if data[:state] + select( + browser: instance, + css: '.active .newTicket select[name="state_id"]', + value: data[:state], + mute_log: true, + ) + end if data[:title] set( browser: instance,