diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index 1aa8177f8..95b03750e 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -87,9 +87,9 @@ class App.TicketOverview extends App.Controller startDragItem: (event) => return if !@batchSupport - @grabbedItem = $(event.currentTarget) - offset = @grabbedItem.offset() - @batchDragger = $(App.view('ticket_overview/batch_dragger')()) + @grabbedItem = $(event.currentTarget) + offset = @grabbedItem.offset() + @batchDragger = $(App.view('ticket_overview/batch_dragger')()) @grabbedItemClone = @grabbedItem.clone() @grabbedItemClone.data('offset', @grabbedItem.offset()) @grabbedItemClone.addClass('batch-dragger-item js-main-item') @@ -642,10 +642,35 @@ class App.TicketOverview extends App.Controller )) renderOptionsMacros: => - macros = App.Macro.search(filter: { active: true }, sortBy:'name', order:'DESC') + + @possibleMacros = [] + macros = App.Macro.search(filter: { active: true }, sortBy:'name', order:'DESC') + + items = @el.find('[name="bulk"]:checked') + + group_ids =[] + for item in items + ticket = App.Ticket.find($(item).val()) + group_ids.push ticket.group_id + + group_ids = _.uniq(group_ids) + + for macro in macros + + # push if no group_ids exists + if _.isEmpty(macro.group_ids) && !_.includes(@possibleMacros, macro) + @possibleMacros.push macro + + # push if group_ids are equal + if _.isEqual(macro.group_ids, group_ids) && !_.includes(@possibleMacros, macro) + @possibleMacros.push macro + + # push if all group_ids of tickets are in macro.group_ids + if !_.isEmpty(macro.group_ids) && _.isEmpty(_.difference(group_ids,macro.group_ids)) && !_.includes(@possibleMacros, macro) + @possibleMacros.push macro @batchMacro.html $(App.view('ticket_overview/batch_overlay_macro')( - macros: macros + macros: @possibleMacros )) active: (state) => diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee index a3c7d485d..48c604ab5 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee @@ -1,7 +1,7 @@ class App.TicketZoomAttributeBar extends App.Controller elements: '.js-submitDropdown': 'buttonDropdown' - '.js-reset': 'resetButton' + '.js-reset': 'resetButton' events: 'mousedown .js-openDropdownMacro': 'toggleMacroMenu' @@ -11,6 +11,7 @@ class App.TicketZoomAttributeBar extends App.Controller 'mouseleave .js-dropdownActionMacro': 'onActionMacroMouseLeave' 'click .js-secondaryAction': 'chooseSecondaryAction' + searchCondition: {} constructor: -> super @@ -24,6 +25,12 @@ class App.TicketZoomAttributeBar extends App.Controller @render() ) + @bind('MacroPreconditionUpdate', (data) => + return if data.taskKey isnt @taskKey + @searchCondition = data.params + @render() + ) + release: => App.Macro.unsubscribe(@subscribeId) @@ -34,16 +41,24 @@ class App.TicketZoomAttributeBar extends App.Controller if @resetButton.get(0) && !@resetButton.hasClass('hide') resetButtonShown = true - macros = App.Macro.findAllByAttribute('active', true) + macros = App.Macro.search(filter: { active: true }, sortBy:'name', order:'DESC') + @macroLastUpdated = App.Macro.lastUpdatedAt() + @possibleMacros = [] if _.isEmpty(macros) || !@permissionCheck('ticket.agent') macroDisabled = true + else + for macro in macros + if !_.isEmpty(macro.group_ids) && @searchCondition.group_id && !_.includes(macro.group_ids, parseInt(@searchCondition.group_id)) + continue + + @possibleMacros.push macro localeEl = $(App.view('ticket_zoom/attribute_bar')( - macros: macros - macroDisabled: macroDisabled - overview_id: @overview_id + macros: @possibleMacros + macroDisabled: macroDisabled + overview_id: @overview_id resetButtonShown: resetButtonShown )) @setSecondaryAction(@secondaryAction, localeEl) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_macro.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_macro.coffee new file mode 100644 index 000000000..75ab4ee7d --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_macro.coffee @@ -0,0 +1,11 @@ +class TicketZoomFormHandlerMacro + + # central method, is getting called on every ticket form change + # but only trigger event for group_id changes + @run: (params, attribute, attributes, classname, form, ui) -> + + return if attribute.name isnt 'group_id' + + App.Event.trigger('MacroPreconditionUpdate', { taskKey: ui.taskKey, params: params }) + +App.Config.set('120-ticketFormMacro', TicketZoomFormHandlerMacro, 'TicketZoomFormHandler') diff --git a/app/assets/javascripts/app/models/macro.coffee b/app/assets/javascripts/app/models/macro.coffee index 882144594..d2bac8d89 100644 --- a/app/assets/javascripts/app/models/macro.coffee +++ b/app/assets/javascripts/app/models/macro.coffee @@ -1,57 +1,26 @@ class App.Macro extends App.Model - @configure 'Macro', 'name', 'perform', 'ux_flow_next_up', 'note', 'active' + @configure 'Macro', 'name', 'perform', 'ux_flow_next_up', 'note', 'group_ids', 'active' @extend Spine.Model.Ajax @url: @apiPath + '/macros' @configure_attributes = [ - { - name: 'name', - display: 'Name', - tag: 'input', - type: 'text', - limit: 100, - null: false + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, + { name: 'perform', display: 'Actions', tag: 'ticket_perform_action', null: true }, - { - name: 'perform', - display: 'Actions', - tag: 'ticket_perform_action', - null: true - }, - { - name: 'ux_flow_next_up', - display: 'Once completed...', - tag: 'select', - default: 'none', - options: { - none: 'Stay on tab', - next_task: 'Close tab', - next_from_overview: 'Advance to next ticket from overview' + { name: 'ux_flow_next_up', display: 'Once completed...', tag: 'select', default: 'none', options: { + none: 'Stay on tab', next_task: 'Close tab', next_from_overview: 'Advance to next ticket from overview' } }, - { - name: 'updated_at', - display: 'Updated', - tag: 'datetime', - readonly: 1 - }, - { - name: 'note', - display: 'Note', - tag: 'textarea', - limit: 250, - null: true - }, - { - name: 'active', - display: 'Active', - tag: 'active', - default: true - }, + { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, + { name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, + { name: 'group_ids', display: 'Groups', tag: 'column_select', relation: 'Group', null: true }, + { name: 'active', display: 'Active', tag: 'active', default: true }, ] @configure_delete = true @configure_clone = true @configure_overview = [ 'name', + 'note', + 'group_ids', ] @description = ''' diff --git a/app/models/macro.rb b/app/models/macro.rb index a29b9b3c7..1004ebe93 100644 --- a/app/models/macro.rb +++ b/app/models/macro.rb @@ -8,4 +8,6 @@ class Macro < ApplicationModel store :perform validates :name, presence: true validates :ux_flow_next_up, inclusion: { in: %w[none next_task next_from_overview] } + + has_and_belongs_to_many :groups, after_add: :cache_update, after_remove: :cache_update, class_name: 'Group' end diff --git a/db/migrate/20190713000001_group_dependent_macros.rb b/db/migrate/20190713000001_group_dependent_macros.rb new file mode 100644 index 000000000..5a28c87f6 --- /dev/null +++ b/db/migrate/20190713000001_group_dependent_macros.rb @@ -0,0 +1,18 @@ +class GroupDependentMacros < ActiveRecord::Migration[4.2] + def up + + create_table :groups_macros, id: false do |t| # rubocop:disable Rails/CreateTableWithTimestamps + t.references :macro, null: false + t.references :group, null: false + end + add_index :groups_macros, [:macro_id] + add_index :groups_macros, [:group_id] + add_foreign_key :groups_macros, :macros + add_foreign_key :groups_macros, :groups + + end + + def self.down + drop_table :groups_macros + end +end diff --git a/spec/factories/macro.rb b/spec/factories/macro.rb index 1af3f900b..2b50ea014 100644 --- a/spec/factories/macro.rb +++ b/spec/factories/macro.rb @@ -1,7 +1,7 @@ FactoryBot.define do factory :macro do sequence(:name) { |n| "Macro #{n}" } - perform { {} } + perform { { 'ticket.state_id' => { 'value' => 1 } } } ux_flow_next_up { 'next_task' } note { '' } active { true } diff --git a/spec/support/capybara/browser_test_helper.rb b/spec/support/capybara/browser_test_helper.rb index 68b47f181..2b9e894aa 100644 --- a/spec/support/capybara/browser_test_helper.rb +++ b/spec/support/capybara/browser_test_helper.rb @@ -27,6 +27,37 @@ module BrowserTestHelper Waiter.new(wait_handle) end + # Moves the mouse from its current position by the given offset. + # If the coordinates provided are outside the viewport (the mouse will end up outside the browser window) + # then the viewport is scrolled to match. + # + # @example + # move_mouse_by(x, y) + # move_mouse_by(100, 200) + # + def move_mouse_by(x_axis, y_axis) + page.driver.browser.action.move_by(x_axis, y_axis).perform + end + + # Clicks and hold (without releasing) in the middle of the given element. + # + # @example + # click_and_hold(ticket) + # click_and_hold(tr[data-id='1']) + # + def click_and_hold(element) + page.driver.browser.action.click_and_hold(element).perform + end + + # Releases the depressed left mouse button at the current mouse location. + # + # @example + # release_mouse + # + def release_mouse + page.driver.browser.action.release.perform + end + class Waiter < SimpleDelegator # This method is a derivation of Selenium::WebDriver::Wait#until diff --git a/spec/support/capybara/common_actions.rb b/spec/support/capybara/common_actions.rb index 60193e1e6..70fa9bc8b 100644 --- a/spec/support/capybara/common_actions.rb +++ b/spec/support/capybara/common_actions.rb @@ -173,6 +173,16 @@ module CommonActions page.driver.browser.navigate.refresh attribute end + + # opens the macro list in the ticket view via click + # + # @example + # open_macro_list + # + def open_macro_list + click '.js-openDropdownMacro' + end + end RSpec.configure do |config| diff --git a/spec/support/capybara/selectors.rb b/spec/support/capybara/selectors.rb index a32e91a9c..32f0507a9 100644 --- a/spec/support/capybara/selectors.rb +++ b/spec/support/capybara/selectors.rb @@ -23,3 +23,15 @@ end Capybara.add_selector(:text_module) do css { |id| %(.shortcut > ul > li[data-id="#{id}"]) } end + +Capybara.add_selector(:macro) do + css { |id| %(.js-submitDropdown > ul > li[data-id="#{id}"]) } +end + +Capybara.add_selector(:macro_batch) do + css { |id| %(.batch-overlay-macro-entry[data-id='#{id}']) } +end + +Capybara.add_selector(:table_row) do + css { |id| %(tr[data-id='#{id}']) } +end diff --git a/spec/system/examples/macros_examples.rb b/spec/system/examples/macros_examples.rb new file mode 100644 index 000000000..90e8ee8f2 --- /dev/null +++ b/spec/system/examples/macros_examples.rb @@ -0,0 +1,45 @@ +RSpec.shared_examples 'macros' do |path:| + + let!(:group1) { create :group } + let!(:group2) { create :group } + let!(:macro_without_group) { create :macro } + let!(:macro_group1) { create :macro, groups: [group1] } + let!(:macro_group2) { create :macro, groups: [group2] } + + it 'supports group-dependent macros' do + + # give user access to all groups including those created + # by using FactoryBot outside of the example + group_names_access_map = Group.all.pluck(:name).each_with_object({}) do |group_name, result| + result[group_name] = 'full'.freeze + end + + current_user do |user| + user.group_names_access_map = group_names_access_map + user.save! + end + + # refresh browser to get macro accessable + refresh + visit path + + within(:active_content) do + + # select group + find('select[name="group_id"]').select(group1.name) + + open_macro_list + expect(page).to have_selector(:macro, macro_without_group.id) + expect(page).to have_selector(:macro, macro_group1.id) + expect(page).to have_no_selector(:macro, macro_group2.id) + + # select group + find('select[name="group_id"]').select(group2.name) + + open_macro_list + expect(page).to have_selector(:macro, macro_without_group.id) + expect(page).to have_no_selector(:macro, macro_group1.id) + expect(page).to have_selector(:macro, macro_group2.id) + end + end +end diff --git a/spec/system/ticket/update_spec.rb b/spec/system/ticket/update_spec.rb index ecd2c2563..c3ab7543c 100644 --- a/spec/system/ticket/update_spec.rb +++ b/spec/system/ticket/update_spec.rb @@ -1,6 +1,7 @@ require 'rails_helper' require 'system/examples/text_modules_examples' +require 'system/examples/macros_examples' RSpec.describe 'Ticket Update', type: :system do @@ -141,4 +142,8 @@ RSpec.describe 'Ticket Update', type: :system do context 'when using text modules' do include_examples 'text modules', path: "#ticket/zoom/#{Ticket.first.id}" end + + context 'when using macros' do + include_examples 'macros', path: "#ticket/zoom/#{Ticket.first.id}" + end end diff --git a/spec/system/ticket/view_spec.rb b/spec/system/ticket/view_spec.rb new file mode 100644 index 000000000..94b5a4670 --- /dev/null +++ b/spec/system/ticket/view_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +RSpec.describe 'Ticket views', type: :system do + + let!(:group1) { create :group } + let!(:group2) { create :group } + let!(:macro_without_group) { create :macro } + let!(:macro_group1) { create :macro, groups: [group1] } + let!(:macro_group2) { create :macro, groups: [group2] } + + it 'supports group-dependent macros' do + + ticket1 = create :ticket, group: group1 + ticket2 = create :ticket, group: group2 + + # give user access to all groups including those created + # by using FactoryBot outside of the example + group_names_access_map = Group.all.pluck(:name).each_with_object({}) do |group_name, result| + result[group_name] = 'full'.freeze + end + + current_user do |user| + user.group_names_access_map = group_names_access_map + user.save! + end + + # refresh browser to get macro accessable + refresh + visit '#ticket/view/all_open' + + within(:active_content) do + + ticket = page.find(:table_row, 1).native + + # click and hold first ticket in table + click_and_hold(ticket) + + # move ticket to y -ticket.location.y + move_mouse_by(0, -ticket.location.y + 5) + + # move a bit to the left to display macro batches + move_mouse_by(-250, 0) + + expect(page).to have_selector(:macro_batch, macro_without_group.id, visible: true) + expect(page).to have_no_selector(:macro_batch, macro_group1.id) + expect(page).to have_no_selector(:macro_batch, macro_group2.id) + + release_mouse + + refresh + + ticket = page.find(:table_row, ticket1.id).native + + # click and hold first ticket in table + click_and_hold(ticket) + + # move ticket to y -ticket.location.y + move_mouse_by(0, -ticket.location.y + 5) + + # move a bit to the left to display macro batches + move_mouse_by(-250, 0) + + expect(page).to have_selector(:macro_batch, macro_without_group.id, visible: true) + expect(page).to have_selector(:macro_batch, macro_group1.id) + expect(page).to have_no_selector(:macro_batch, macro_group2.id) + + release_mouse + + refresh + + ticket = page.find(:table_row, ticket2.id).native + + # click and hold first ticket in table + click_and_hold(ticket) + + # move ticket to y -ticket.location.y + move_mouse_by(0, -ticket.location.y + 5) + + # move a bit to the left to display macro batches + move_mouse_by(-250, 0) + + expect(page).to have_selector(:macro_batch, macro_without_group.id, visible: true) + expect(page).to have_no_selector(:macro_batch, macro_group1.id) + expect(page).to have_selector(:macro_batch, macro_group2.id) + + end + end +end