Browse Source

Rework sidebar to use spine-zammad style requests instead of custom code (#4886).

Co-authored-by: Rolf Schmidt <rolf.schmidt@zammad.com>
Florian Liebe 7 months ago
parent
commit
dd32445aec

+ 19 - 28
app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist.coffee

@@ -32,12 +32,8 @@ class SidebarChecklist extends App.Controller
     new ChecklistRemoveModal(
       container: @elSidebar.closest('.content')
       callback:  =>
-        @ajax(
-          id:   'checklist_ticket_remove_checklist'
-          type: 'DELETE'
-          url:  "#{@apiPath}/tickets/#{@ticket.id}/checklist"
-          processData: true
-          success: (data, status, xhr) =>
+        @checklist.destroy(
+          done: =>
             @clearWidget()
 
             @widget = new App.SidebarChecklistStart(el: @elSidebar, parentVC: @)
@@ -64,10 +60,7 @@ class SidebarChecklist extends App.Controller
 
     @startLoading()
 
-  delayedShown: =>
-    @delay(@shown, 250, 'sidebar-checklist')
-
-  shown: =>
+  shown: (enterEditMode = false) =>
     @startLoading()
 
     @ajax(
@@ -84,7 +77,18 @@ class SidebarChecklist extends App.Controller
 
           @checklist = App.Checklist.find(data.id)
 
-          @widget = new App.SidebarChecklistShow(el: @elSidebar, parentVC: @, checklist: @checklist, readOnly: !@changeable, enterEditMode: false)
+          @widget = new App.SidebarChecklistShow(el: @elSidebar, parentVC: @, checklist: @checklist, readOnly: !@changeable, enterEditMode: enterEditMode)
+
+          if @subscribeId
+            App.Checklist.unsubscribeItem(@subscribeId)
+
+          @subscribeId = App.Checklist.subscribeItem(
+            data.id,
+            (item) =>
+              return if item.updated_by_id is App.Session.get().id
+              return if @widget.actionController
+              @shown()
+          )
         else
           @widget = new App.SidebarChecklistStart(el: @elSidebar, parentVC: @, readOnly: !@changeable)
 
@@ -93,16 +97,12 @@ class SidebarChecklist extends App.Controller
 
     return if @subcribed
     @subcribed = true
-    @controllerBind('Checklist:destroy', (data) =>
-      return if @ticket_id && @ticket_id isnt data.ticket_id
-      @delayedShown()
-    )
-    @controllerBind('Checklist:create Checklist:update Checklist:touch ChecklistItem:create ChecklistItem:update ChecklistItem:touch ChecklistItem:destroy', (data) =>
+
+    @controllerBind('Checklist:create Checklist:destroy', (data) =>
       return if @ticket_id && @ticket_id isnt data.ticket_id
       return if data.updated_by_id is App.Session.get().id
-      return if @widget?.itemEditInProgress
-      return if @widget?.titleChangeInProgress
-      @delayedShown()
+      return if @widget.actionController
+      @shown()
     )
 
   clearWidget: =>
@@ -112,15 +112,6 @@ class SidebarChecklist extends App.Controller
     @checklist = undefined
     @renderActions()
 
-  switchToChecklist: (id, enterEditMode = false) =>
-    @clearWidget()
-
-    @checklist = App.Checklist.find(id)
-
-    @renderActions()
-
-    @widget = new App.SidebarChecklistShow(el: @elSidebar, parentVC: @, checklist: @checklist, enterEditMode: enterEditMode)
-
 class ChecklistRemoveModal extends App.ControllerGenericDestroyConfirm
   onSubmit: =>
     @close()

+ 114 - 160
app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist_show.coffee

@@ -18,69 +18,54 @@ class App.SidebarChecklistShow extends App.Controller
 
   constructor: ->
     super
-
-    @objects = @checklist.sorted_items()
     @render()
 
   render: ->
+
     @html App.view('ticket_zoom/sidebar_checklist_show')(
       checklistTitle: @checklistTitle()
       readOnly: @readOnly
     )
 
-    @el.parent().off('click').on('click', (e) =>
-      if @itemEditInProgress
-        @clearEditWidget()
-        @itemEditInProgress = false
+    $('body').off('click').on('click', (e) =>
+      return if @actionController && @actionController.constructor.name is 'ChecklistReorder'
+      return if $(e.target).closest('.js-actions').length > 0
+      return if $(e.target).closest('.checklistShowButtons div.btn').length > 0
+      return if $(e.target).closest('.checkbox-replacement').length > 0
 
-      if @titleChangeInProgress
-        @clearRenameWidget()
-        @titleChangeInProgress = false
+      @actionController?.releaseController()
     )
 
     @renderTable()
 
-    if @enterEditMode
-      cell = @table.find('tr:last-of-type').find('.checklistItemValue')[0]
-      row  = $(cell).closest('tr')
-
-      @activateItemEditMode(cell, row, row.data('id'))
-
   checklistTitle: =>
     @checklist.name || App.i18n.translateInline('%s Checklist', App.Config.get('ticket_hook') + @parentVC.ticket.number)
 
   onReorder: (e) =>
-    @clearEditWidget()
-    @toggleReorder(true)
+    @preventDefaultAndStopPropagation(e)
+    @actionController?.releaseController()
+    @actionController = new ChecklistReorder(parentVC: @)
 
   onAdd: (e) =>
     @addButton.attr('disabled', true)
-    @itemEditInProgress = true
-
-    @ajax(
-      id:   'checklist_item_create'
-      type: 'POST'
-      url:  "#{@apiPath}/tickets/#{@parentVC.ticket.id}/checklist/items"
-      data: JSON.stringify({text: ''})
-      processData: true
-      success: (data, status, xhr) =>
-        App.Collection.loadAssets(data.assets)
-        @objects = @checklist.sorted_items()
-
-        @clearEditWidget()
-        @renderTable()
-        cell = @table.find('tr:last-of-type').find('.checklistItemValue')[0]
-        row  = $(cell).closest('tr')
 
-        @activateItemEditMode(cell, row, data.id)
-
-        @addButton.attr('disabled', false)
-      error: =>
+    callbackDone = (data) =>
+      @enterEditModeId = data.id
+      @renderTable()
+      @addButton.attr('disabled', false)
+
+    item = new App.ChecklistItem
+    item.checklist_id = @checklist.id
+    item.text = ''
+    item.save(
+      done: ->
+        App.ChecklistItem.full(@id, callbackDone, force: true)
+      fail: =>
+        @renderTable()
         @addButton.attr('disabled', false)
     )
 
   onCheckboxClick: (e) =>
-    @clearEditWidget()
     upcomingState = e.currentTarget.checked
     id = parseInt(e.currentTarget.value)
 
@@ -92,7 +77,6 @@ class App.SidebarChecklistShow extends App.Controller
     row  = $(e.currentTarget).closest('tr')
     checkbox = row.find('.js-checkbox')[0]
 
-    @clearEditWidget()
     upcomingState = !checkbox.checked
 
     checkbox.disabled = true
@@ -100,21 +84,14 @@ class App.SidebarChecklistShow extends App.Controller
     @updateChecklistItem(id, upcomingState, checkbox)
 
   updateChecklistItem: (id, upcomingState, checkboxElem) =>
-    @ajax(
-      id:   'checklist_item_update_checked'
-      type: 'PATCH'
-      url:  "#{@apiPath}/tickets/#{@parentVC.ticket.id}/checklist/items/#{id}"
-      data: JSON.stringify({checked: upcomingState})
-      success: (data, status, xhr) =>
-        object = _.find @objects, (elem) -> elem.id == id
-
-        object.load(checked: upcomingState)
-
+    item = App.ChecklistItem.find(id)
+    item.checked = upcomingState
+    item.save(
+      done: =>
         checkboxElem.disabled = false
         @renderTable()
-      error: ->
-        checkboxElem.checked = !upcomingState
-        checkboxElem.disabled = false
+      fail: ->
+        @renderTable()
     )
 
   onSaveOrder: (e) =>
@@ -122,60 +99,43 @@ class App.SidebarChecklistShow extends App.Controller
 
     sorted_item_ids = @table.find('tbody tr').toArray().map (elem) -> elem.dataset.id
 
-    @ajax(
-      id:   'checklist_update'
-      type: 'PATCH'
-      url:  "#{@apiPath}/tickets/#{@parentVC.ticket.id}/checklist"
-      data: JSON.stringify({sorted_item_ids: sorted_item_ids})
-      processData: true
-      success: (data, status, xhr) =>
-        App.Collection.loadAssets(data.assets)
-        @toggleReorder(false)
-        @saveOrderButton.attr('disabled', false)
-      error: =>
-        @saveOrderButton.attr('disabled', false)
+    item = @checklist
+    item.sorted_item_ids = sorted_item_ids
+    item.save(
+      done: (data) =>
+        @actionController?.completed()
+      fail: =>
+        @actionController?.releaseController()
     )
 
   onResetOrder: (e) =>
-    @toggleReorder(false, 'cancel')
+    @actionController?.releaseController()
 
   onAction: (e) =>
     e.stopPropagation()
 
-    if @itemEditInProgress
-      @clearEditWidget()
-      @itemEditInProgress = false
-
-    if @titleChangeInProgress
-      @clearRenameWidget()
-      @titleChangeInProgress = false
-
     dropdown = $(e.currentTarget).closest('td').find('.js-table-action-menu')
     dropdown.dropdown('toggle')
-    dropdown.on('click.dropdown', '[data-table-action]', @onActionButtonClicked)
+    dropdown.off('click.dropdown').on('click.dropdown', '[data-table-action]', @onActionButtonClicked)
 
   onTitleChange: (e) =>
     e?.stopPropagation()
+    return if @actionController && @actionController.constructor.name is 'ChecklistReorder'
 
     # Close any open dropdowns
     @el.find('.dropdown--actions.open').dropdown('toggle')
 
-    if @itemEditInProgress
-      @clearEditWidget()
-      @itemEditInProgress = false
-
-    return if @titleChangeInProgress
-
     if e
       elem = e.currentTarget
     else
       elem = @el.find('.js-title')[0]
 
-    @clearRenameWidget()
-    @renameWidget = new ChecklistRenameEdit(el: elem, parentVC: @, originalValue: @checklistTitle())
+    @actionController?.releaseController()
+    @actionController = new ChecklistRenameEdit(el: elem, parentVC: @, originalValue: @checklistTitle())
 
   onActionButtonClicked: (e) =>
     e?.stopPropagation()
+    return if @actionController && @actionController.constructor.name is 'ChecklistReorder'
 
     id = $(e.currentTarget).parents('tr').data('id')
     name = e.currentTarget.getAttribute('data-table-action')
@@ -207,17 +167,11 @@ class App.SidebarChecklistShow extends App.Controller
   onEditChecklistItem: (id, e) =>
     @preventDefaultAndStopPropagation(e)
 
-    if @titleChangeInProgress
-      @clearRenameWidget()
-      @titleChangeInProgress = false
-
-    return if @itemEditInProgress && @itemEditInProgress == id
+    return if @actionController && @actionController.constructor.name is 'ChecklistItemEdit' && @actionController.id == id
 
     row  = $(e.currentTarget).closest('tr')
     cell = row.find('.checklistItemValue')[0]
 
-    @clearEditWidget()
-
     @activateItemEditMode(cell, row, id)
 
   onDeleteChecklistItem: (id, e) =>
@@ -231,17 +185,8 @@ class App.SidebarChecklistShow extends App.Controller
     item = App.ChecklistItem.find(id)
 
     deleteCallback = =>
-      @ajax(
-        id:   'checklist_item_delete'
-        type: 'DELETE'
-        url:  "#{@apiPath}/tickets/#{@parentVC.ticket.id}/checklist/items/#{id}"
-        processData: true
-        success: (data, status, xhr) =>
-          App.ChecklistItem.find(id).remove(clear: true)
-
-          @objects = @checklist.sorted_items()
-
-          @clearEditWidget()
+      item.destroy(
+        done: =>
           @renderTable()
       )
 
@@ -257,34 +202,20 @@ class App.SidebarChecklistShow extends App.Controller
     )
 
   activateItemEditMode: (cell, row, id) =>
-    $(cell).addClass('edit-widget-active')
-    $(row).find('.dropdown').addClass('hide')
-
-    @editWidget = new ChecklistItemEdit(
+    @actionController?.releaseController()
+    @actionController = new ChecklistItemEdit(
       el: cell
       parentVC: @
       originalValue: cell.textContent.trim()
       id: id
-      cancelCallback: ->
-        $(cell).removeClass('edit-widget-active')
-        $(row).find('.dropdown').removeClass('hide')
-
-        if $(row).find('.dropdown--actions').hasClass('open')
-          $(row).find('.js-table-action-menu').dropdown('toggle')
     )
 
-  clearEditWidget: =>
-    @editWidget?.onCancel()
-    @editWidget = undefined
-
-  clearRenameWidget: =>
-    @renameWidget?.onCancel()
-    @renameWidget = undefined
-
   renderTable: ->
     @table.find('tbody').empty()
 
-    for object in @objects
+    sorted_items = @checklist.sorted_items()
+
+    for object in sorted_items
       html = App.view('ticket_zoom/sidebar_checklist_show_row')(
         object: object
         readOnly: @readOnly
@@ -292,7 +223,7 @@ class App.SidebarChecklistShow extends App.Controller
 
       @table.find('tbody').append(html)
 
-    if !@objects.length
+    if !sorted_items.length
       html = App.view('ticket_zoom/sidebar_checklist_show_no_items')()
       @table.find('tbody').append(html)
 
@@ -305,7 +236,17 @@ class App.SidebarChecklistShow extends App.Controller
     @table.find('tbody').sortable(dndOptions)
     @table.find('tbody').sortable('disable')
 
-    @reorderButton.toggleClass('hide', !@objects.length || @objects.length < 2)
+    @reorderButton.toggleClass('hide', !sorted_items.length || sorted_items.length < 2)
+
+    if @enterEditMode
+      @enterEditMode   = undefined
+      @enterEditModeId = @table.find('tr:last-of-type').data('id')
+
+    if @enterEditModeId
+      cell                = @table.find("tr[data-id='" + @enterEditModeId + "']").find('.checklistItemValue')[0]
+      row                 = $(cell).closest('tr')
+      @enterEditModeId = undefined
+      @activateItemEditMode(cell, row, row.data('id'))
 
 class ChecklistItemEdit extends App.Controller
   elements:
@@ -318,47 +259,50 @@ class ChecklistItemEdit extends App.Controller
   constructor: ->
     super
 
-    @parentVC.itemEditInProgress = @id
-
     @render()
 
+  releaseController: =>
+    super
+
+    @el.text(@originalValue)
+    @el.removeClass('edit-widget-active')
+    @el.closest('tr').find('.dropdown').removeClass('hide')
+
+    if @el.closest('tr').find('.dropdown--actions').hasClass('open')
+      @el.closest('tr').find('.js-table-action-menu').dropdown('toggle')
+
+    @parentVC.actionController = undefined
+
   render: =>
     @html App.view('ticket_zoom/sidebar_checklist_item_edit')(value: @object()?.text)
+
+    @el.addClass('edit-widget-active')
+    @el.closest('tr').find('.dropdown').addClass('hide')
     @input.focus().val('').val(@object()?.text)
 
   object: =>
-    _.find @parentVC.objects, (elem) => elem.id == @id
+    App.ChecklistItem.find(@id)
 
   onCancel: (e) =>
     @preventDefaultAndStopPropagation(e)
-
-    @release()
-    @el.html(App.Utils.linkify(@originalValue))
-    @parentVC.itemEditInProgress = null
-
-    @cancelCallback() if @cancelCallback
+    @releaseController()
 
   onConfirm: (e) =>
     @preventDefaultAndStopPropagation(e)
 
-    @ajax(
-      id:   'checklist_item_update_text'
-      type: 'PATCH'
-      url:  "#{@apiPath}/tickets/#{@parentVC.parentVC.ticket.id}/checklist/items/#{@id}"
-      data: JSON.stringify({text: @input.val()})
-      processData: true
-      success: (data, status, xhr) =>
-        @object().load(text: @input.val())
-
-        @parentVC.clearEditWidget()
+    item = @object()
+    item.text = @input.val()
+    item.save(
+      done: =>
+        @parentVC.renderTable()
+      fail: =>
         @parentVC.renderTable()
-        @parentVC.itemEditInProgress = null
     )
 
   onKeyUp: (e) =>
     switch e.key
       when 'Enter' then @onConfirm()
-      when 'Escape' then @onCancel()
+      when 'Escape' then @releaseController()
 
 class ChecklistRenameEdit extends App.Controller
   elements:
@@ -370,11 +314,13 @@ class ChecklistRenameEdit extends App.Controller
 
   constructor: ->
     super
-
-    @parentVC.titleChangeInProgress = true
-
     @render()
 
+  releaseController: =>
+    super
+    @el.text(@originalValue)
+    @parentVC.actionController = undefined
+
   render: =>
     @html App.view('ticket_zoom/sidebar_checklist_title_edit')(
       value: @object()?.name
@@ -387,26 +333,16 @@ class ChecklistRenameEdit extends App.Controller
 
   onCancel: (e) =>
     @preventDefaultAndStopPropagation(e)
-
-    @release()
-    @el.text(@originalValue)
-    @parentVC.titleChangeInProgress = false
+    @releaseController()
 
   onConfirm: (e) =>
     @preventDefaultAndStopPropagation(e)
 
-    @ajax(
-      id:   'checklist_title_update'
-      type: 'PATCH'
-      url:  "#{@apiPath}/tickets/#{@parentVC.parentVC.ticket.id}/checklist"
-      data: JSON.stringify({name: @input.val()})
-      processData: true
-      success: (data, status, xhr) =>
-        @object().load(name: @input.val())
-
-        @parentVC.clearRenameWidget()
+    checklist = @object()
+    checklist.name = @input.val()
+    checklist.save(
+      done: =>
         @parentVC.render()
-        @parentVC.titleChangeInProgress = false
     )
 
   onKeyUp: (e) =>
@@ -414,6 +350,24 @@ class ChecklistRenameEdit extends App.Controller
       when 'Enter' then @onConfirm()
       when 'Escape' then @onCancel()
 
+class ChecklistReorder extends App.Controller
+  constructor: ->
+    super
+    @render()
+
+  releaseController: =>
+    @parentVC.toggleReorder(false, 'cancel')
+    @parentVC.saveOrderButton.attr('disabled', false)
+    @parentVC.actionController = undefined
+
+  completed: =>
+    @parentVC.toggleReorder(false)
+    @parentVC.saveOrderButton.attr('disabled', false)
+    @parentVC.actionController = undefined
+
+  render: =>
+    @parentVC.toggleReorder(true)
+
 class ChecklistItemRemoveModal extends App.ControllerGenericDestroyConfirm
   onSubmit: =>
     @close()

+ 2 - 4
app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist_start.coffee

@@ -36,8 +36,7 @@ class App.SidebarChecklistStart extends App.Controller
       url:  "#{@apiPath}/tickets/#{@parentVC.ticket.id}/checklist"
       processData: true
       success: (data, status, xhr) =>
-        App.Collection.loadAssets(data.assets)
-        @parentVC.switchToChecklist(data.id, true)
+        @parentVC.shown(true)
     )
 
   onAddFromTemplate: (e) =>
@@ -56,8 +55,7 @@ class App.SidebarChecklistStart extends App.Controller
       url:  "#{@apiPath}/tickets/#{@parentVC.ticket.id}/checklist"
       data: JSON.stringify({ ticket_id: @parentVC.ticket.id, template_id: params.checklist_template_id })
       success: (data, status, xhr) =>
-        App.Collection.loadAssets(data.assets)
-        @parentVC.switchToChecklist(data.id)
+        @parentVC.shown()
     )
 
   onTemplateChange: (e) =>

+ 1 - 1
app/assets/javascripts/app/models/checklist.coffee

@@ -1,5 +1,5 @@
 class App.Checklist extends App.Model
-  @configure 'Checklist', 'name', 'items', 'active', 'updated_at'
+  @configure 'Checklist', 'name', 'sorted_item_ids', 'active', 'updated_at'
   @extend Spine.Model.Ajax
   @url: @apiPath + '/checklists'
 

+ 4 - 4
app/models/checklist.rb

@@ -37,10 +37,10 @@ class Checklist < ApplicationModel
 
   def notify_clients_data_attributes
     {
-      id:         id,
-      ticket_id:  ticket_id,
-      updated_at: updated_at,
-      updated_by: updated_by_id,
+      id:            id,
+      ticket_id:     ticket_id,
+      updated_at:    updated_at,
+      updated_by_id: updated_by_id,
     }
   end
 

+ 7 - 5
app/models/checklist/item.rb

@@ -12,6 +12,7 @@ class Checklist::Item < ApplicationModel
   scope :for_user, ->(user) { joins(checklist: :ticket).where(tickets: { group: user.group_ids_access('read') }) }
 
   after_create :update_checklist
+  after_update :update_checklist
   after_destroy :update_checklist
 
   validates :text, presence: { allow_blank: true }
@@ -42,12 +43,13 @@ class Checklist::Item < ApplicationModel
   private
 
   def update_checklist
-    if persisted? && checklist.sorted_item_ids.exclude?(id.to_s)
-      checklist.sorted_item_ids << id
-    end
-    if !persisted?
-      checklist.sorted_item_ids = checklist.sorted_item_ids.reject { |sid| sid.to_s == id.to_s }
+    if persisted?
+      checklist.sorted_item_ids |= [id.to_s]
+    else
+      checklist.sorted_item_ids -= [id.to_s]
     end
+    checklist.updated_at    = Time.zone.now
+    checklist.updated_by_id = UserInfo.current_user_id || updated_by_id
     checklist.save!
   end
 end

+ 3 - 3
i18n/zammad.pot

@@ -134,7 +134,7 @@ msgstr ""
 msgid "%s Attribute"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist_show.coffee:50
+#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist_show.coffee:42
 #: app/assets/javascripts/app/views/ticket_zoom/sidebar_checklist_title_edit.jst.eco:6
 #: app/frontend/apps/desktop/pages/ticket/composables/useTicketChecklist.ts:133
 msgid "%s Checklist"
@@ -2607,7 +2607,7 @@ msgstr ""
 msgid "Check this box if you want to customise how options are sorted. If the box is not checked, values are sorted in alphabetical order."
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist.coffee:57
+#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist.coffee:53
 #: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/TicketSidebarChecklistContent.vue:20
 #: app/frontend/apps/desktop/pages/ticket/components/TicketSidebar/plugins/checklist.ts:12
 msgid "Checklist"
@@ -10820,7 +10820,7 @@ msgstr ""
 msgid "Please save your recovery codes listed below somewhere safe. You can use them to sign in if you lose access to another two-factor method:"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist_start.coffee:74
+#: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_checklist_start.coffee:72
 msgid "Please select a checklist template."
 msgstr ""
 

+ 58 - 8
spec/system/ticket/zoom/checklist_spec.rb

@@ -3,14 +3,15 @@
 require 'rails_helper'
 
 RSpec.describe 'Ticket zoom > Checklist', authenticated_as: :authenticate, type: :system do
-  let(:ticket) { create(:ticket, group: Group.first) }
+  let(:other_agent) { create(:agent, groups: Group.all) }
+  let(:ticket)      { create(:ticket, group: Group.first) }
 
   def authenticate
     Setting.set('checklist', true)
     true
   end
 
-  def click_checklist_action(id, action)
+  def perform_item_action(id, action)
     page.find(".checklistShow tr[data-id='#{id}'] .js-action", wait: 0).click
     page.find(".checklistShow tr[data-id='#{id}'] li[data-table-action='#{action}']", wait: 0).click
   rescue => e
@@ -22,6 +23,18 @@ RSpec.describe 'Ticket zoom > Checklist', authenticated_as: :authenticate, type:
     retry
   end
 
+  def perform_checklist_action(text)
+    click '.sidebar[data-tab=checklist] .js-actions'
+    click_on text
+  rescue => e
+    retry_click ||= 5
+    retry_click -= 1
+    sleep 1
+    raise e if retry_click < 1
+
+    retry
+  end
+
   before do
     visit "#ticket/zoom/#{ticket.id}"
   end
@@ -61,33 +74,70 @@ RSpec.describe 'Ticket zoom > Checklist', authenticated_as: :authenticate, type:
       expect(page).to have_button('Add empty checklist')
     end
 
+    it 'does remove the checklist' do
+      perform_checklist_action('Remove checklist')
+      click_on 'delete'
+      expect(page).to have_text('Add empty checklist')
+    end
+
+    it 'does rename the checklist' do
+      perform_checklist_action('Rename checklist')
+      checklist_name = SecureRandom.uuid
+      find('#checklistTitleEditText').fill_in with: checklist_name, fill_options: { clear: :backspace }
+      page.find('.js-confirm').click
+      wait.until { checklist.reload.name == checklist_name }
+    end
+
     it 'does add item' do
       find('.checklistShowButtons .js-add').click
       wait.until { checklist.items.last.text == '' }
     end
 
     it 'does check item' do
-      click_checklist_action(item.id, 'check')
+      perform_item_action(item.id, 'check')
 
       wait.until { item.reload.checked == true }
     end
 
     it 'does uncheck item' do
       item.update(checked: true)
-      click_checklist_action(item.id, 'uncheck')
+      perform_item_action(item.id, 'uncheck')
       wait.until { item.reload.checked == false }
     end
 
     it 'does edit item' do
-      click_checklist_action(item.id, 'edit')
+      perform_item_action(item.id, 'edit')
       item_text = SecureRandom.uuid
       find(".checklistShow tr[data-id='#{item.id}'] .js-input").fill_in with: item_text, fill_options: { clear: :backspace }
       page.find('.js-confirm').click
       wait.until { item.reload.text == item_text }
     end
 
+    it 'does not abort edit when subscription is updating but including it afterwards' do
+      perform_item_action(item.id, 'edit')
+      item_text = SecureRandom.uuid
+      find(".checklistShow tr[data-id='#{item.id}'] .js-input").fill_in with: item_text, fill_options: { clear: :backspace }
+
+      # simulate other users change
+      other_item_text = SecureRandom.uuid
+      checklist.items.create!(text: other_item_text, created_by: other_agent, updated_by: other_agent)
+
+      # not really another way to be absolutely sure that this works
+      sleep 5
+
+      # the new item will be synced after saving
+      expect(page).to have_no_text(other_item_text)
+
+      # it's important that the old edit mode does not abort
+      page.find('.js-confirm').click
+
+      # then both items arrive in the UI
+      expect(page).to have_text(item_text)
+      expect(page).to have_text(other_item_text)
+    end
+
     it 'does delete item' do
-      click_checklist_action(item.id, 'delete')
+      perform_item_action(item.id, 'delete')
       click_on 'delete'
       wait.until { Checklist::Item.find_by(id: item.id).blank? }
     end
@@ -101,7 +151,7 @@ RSpec.describe 'Ticket zoom > Checklist', authenticated_as: :authenticate, type:
 
       it 'does edit item with link' do
         expect(page).to have_link('google.de')
-        click_checklist_action(item.id, 'edit')
+        perform_item_action(item.id, 'edit')
         item_text = SecureRandom.uuid
         find(".checklistShow tr[data-id='#{item.id}'] .js-input").fill_in with: item_text, fill_options: { clear: :backspace }
         page.find('.js-confirm').click
@@ -128,7 +178,7 @@ RSpec.describe 'Ticket zoom > Checklist', authenticated_as: :authenticate, type:
       expect(page).to have_text('Please select a checklist template.')
 
       select checklist_template.name, from: 'checklist_template_id'
-      expect(page).to have_no_text('Please select a checklist template.')
+      wait.until { page.has_no_content?('Please select a checklist template.') }
       click_on('Add from a template')
 
       wait.until { Checklist.where(ticket: ticket).present? }