diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index 9245d82b1..e7decf400 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -358,6 +358,7 @@ class App.TicketCreate extends App.Controller data: config: App.Config.all() user: App.Session.get() + taskKey: @taskKey ) $('#tags').tokenfield() diff --git a/app/assets/javascripts/app/controllers/text_module.coffee b/app/assets/javascripts/app/controllers/text_module.coffee index f6c989134..2aa86fa2b 100644 --- a/app/assets/javascripts/app/controllers/text_module.coffee +++ b/app/assets/javascripts/app/controllers/text_module.coffee @@ -15,16 +15,16 @@ class Index extends App.ControllerSubContent deleteOption: true ) pageData: - home: 'text_modules' - object: 'TextModule' - objects: 'Text modules' + home: 'text_modules' + object: 'TextModule' + objects: 'Text modules' navupdate: '#text_modules' - notes: [ + notes: [ 'Text modules are ...' ] buttons: [ - { name: 'Import', 'data-type': 'import', class: 'btn' } - { name: 'New text module', 'data-type': 'new', class: 'btn--success' } + { name: 'Import', 'data-type': 'import', class: 'btn' } + { name: 'New text module', 'data-type': 'new', class: 'btn--success' } ] container: @el.closest('.content') ) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index d4e66b40b..4a600ac68 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -236,8 +236,9 @@ class App.TicketZoomArticleNew extends App.Controller el: @$('.js-textarea').parent() data: ticket: ticket - user: App.Session.get() + user: App.Session.get() config: App.Config.all() + taskKey: @taskKey ) callback = (ticket) -> textModule.reload( diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_text_module.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_text_module.coffee new file mode 100644 index 000000000..667cd5bd7 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_text_module.coffee @@ -0,0 +1,12 @@ +class TicketZoomFormHandlerTextModule + + # 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('TextModulePreconditionUpdate', { taskKey: ui.taskKey, params: params }) + +App.Config.set('110-ticketFormTextModule', TicketZoomFormHandlerTextModule, 'TicketZoomFormHandler') +App.Config.set('110-ticketFormTextModule', TicketZoomFormHandlerTextModule, 'TicketCreateFormHandler') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index cc7c8f57b..fd345caa8 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -29,6 +29,7 @@ class Edit extends App.ObserverController formMeta: @formMeta params: defaults isDisabled: !ticket.editable() + taskKey: @taskKey #bookmarkable: true ) else @@ -41,6 +42,7 @@ class Edit extends App.ObserverController formMeta: @formMeta params: defaults isDisabled: ticket.editable() + taskKey: @taskKey #bookmarkable: true ) @@ -122,6 +124,7 @@ class SidebarTicket extends App.Controller taskGet: @taskGet formMeta: @formMeta markForm: @markForm + taskKey: @taskKey ) if @permissionCheck('ticket.agent') diff --git a/app/assets/javascripts/app/controllers/widget/text_module.coffee b/app/assets/javascripts/app/controllers/widget/text_module.coffee index 09dc5468c..a9e8df217 100644 --- a/app/assets/javascripts/app/controllers/widget/text_module.coffee +++ b/app/assets/javascripts/app/controllers/widget/text_module.coffee @@ -1,4 +1,5 @@ class App.WidgetTextModule extends App.Controller + searchCondition: {} constructor: -> super @@ -18,6 +19,12 @@ class App.WidgetTextModule extends App.Controller @subscribeId = App.TextModule.subscribe(@update, initFetch: true) + @bind('TextModulePreconditionUpdate', (data) => + return if data.taskKey isnt @taskKey + @searchCondition = data.params + @update() + ) + release: => App.TextModule.unsubscribe(@subscribeId) @@ -26,17 +33,27 @@ class App.WidgetTextModule extends App.Controller @data = data @update() + currentCollection: => + @all + update: => allRaw = App.TextModule.all() - all = [] + @all = [] + for item in allRaw - if item.active is true - attributes = item.attributes() - attributes.content = App.Utils.replaceTags(attributes.content, @data) - all.push attributes + + if item.active isnt true + continue + + if !_.isEmpty(item.group_ids) && @searchCondition.group_id && !_.includes(item.group_ids, parseInt(@searchCondition.group_id)) + continue + + attributes = item.attributes() + attributes.content = App.Utils.replaceTags(attributes.content, @data) + @all.push attributes # set new data if @bindElements[0] for element in @bindElements if $(element).data().plugin_textmodule - $(element).data().plugin_textmodule.collection = all + $(element).data().plugin_textmodule.collection = @all diff --git a/app/assets/javascripts/app/models/text_module.coffee b/app/assets/javascripts/app/models/text_module.coffee index e09958de0..5bb1f223d 100644 --- a/app/assets/javascripts/app/models/text_module.coffee +++ b/app/assets/javascripts/app/models/text_module.coffee @@ -3,8 +3,8 @@ class App.TextModule extends App.Model @extend Spine.Model.Ajax @url: @apiPath + '/text_modules' @configure_attributes = [ - { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, - { name: 'keywords', display: 'Keywords', tag: 'input', type: 'text', limit: 100, null: true }, + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, + { name: 'keywords', display: 'Keywords', tag: 'input', type: 'text', limit: 100, null: true }, { name: 'content', display: 'Content', tag: 'richtext', limit: 2000, null: false, plugins: [ { controller: 'WidgetPlaceholder' @@ -24,6 +24,7 @@ class App.TextModule extends App.Model } ], note: 'To select placeholders from a list, just enter "::".'}, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, + { name: 'group_ids', display: 'Groups', tag: 'column_select', relation: 'Group', null: true }, { name: 'active', display: 'Active', tag: 'active', default: true }, ] @configure_delete = true @@ -32,6 +33,7 @@ class App.TextModule extends App.Model 'name', 'keywords', 'content', + 'group_ids', ] # coffeelint: disable=no_interpolation_in_single_quotes diff --git a/app/models/text_module.rb b/app/models/text_module.rb index 574fa2aa1..89f49d2e4 100644 --- a/app/models/text_module.rb +++ b/app/models/text_module.rb @@ -15,6 +15,8 @@ class TextModule < ApplicationModel csv_delete_possible true + has_and_belongs_to_many :groups, after_add: :cache_update, after_remove: :cache_update, class_name: 'Group' + =begin load text modules from online diff --git a/app/views/tests/text_module.html.erb b/app/views/tests/text_module.html.erb new file mode 100644 index 000000000..e41a8208d --- /dev/null +++ b/app/views/tests/text_module.html.erb @@ -0,0 +1,25 @@ + + + + + + + + + +
+ +
+
+
+
+
+
+ +
+
diff --git a/config/routes/test.rb b/config/routes/test.rb index e6c0afb63..cb74f7deb 100644 --- a/config/routes/test.rb +++ b/config/routes/test.rb @@ -21,6 +21,7 @@ Zammad::Application.routes.draw do match '/tests_html_utils', to: 'tests#html_utils', via: :get match '/tests_ticket_selector', to: 'tests#ticket_selector', via: :get match '/tests_taskbar', to: 'tests#taskbar', via: :get + match '/tests_text_module', to: 'tests#text_module', via: :get match '/tests/wait/:sec', to: 'tests#wait', via: :get match '/tests/unprocessable_entity', to: 'tests#error_unprocessable_entity', via: :get match '/tests/not_authorized', to: 'tests#error_not_authorized', via: :get diff --git a/db/migrate/20190613000001_group_dependent_text_modules.rb b/db/migrate/20190613000001_group_dependent_text_modules.rb new file mode 100644 index 000000000..1fa123508 --- /dev/null +++ b/db/migrate/20190613000001_group_dependent_text_modules.rb @@ -0,0 +1,5 @@ +class GroupDependentTextModules < ActiveRecord::Migration[5.1] + def change + rename_table :text_modules_groups, :groups_text_modules + end +end diff --git a/public/assets/tests/text_module.js b/public/assets/tests/text_module.js new file mode 100644 index 000000000..06d023a4e --- /dev/null +++ b/public/assets/tests/text_module.js @@ -0,0 +1,103 @@ +// text module +test('test text module behaviour with group_ids', function() { + + // active textmodule without group_ids + App.TextModule.refresh([ + { + id: 1, + name: 'main', + keywords: 'keywordsmain', + content: 'contentmain', + active: true, + }, + { + id: 2, + name: 'test2', + keywords: 'keywords2', + content: 'content2', + active: false, + }, + { + id: 3, + name: 'test3', + keywords: 'keywords3', + content: 'content3', + active: true, + group_ids: [1,2], + }, + { + id: 4, + name: 'test4', + keywords: 'keywords4', + content: 'content4', + active: false, + group_ids: [1,2], + }, + ]) + + var textModule = new App.WidgetTextModule({ + el: $('.js-textarea').parent(), + data:{ + user: App.Session.get(), + config: App.Config.all(), + }, + taskKey: 'test1', + }) + + var currentCollection = textModule.currentCollection(); + + equal(currentCollection.length, 2, 'active textmodule') + equal(currentCollection[0].id, 1) + equal(currentCollection[1].id, 3) + + // trigered TextModulePreconditionUpdate with group_id + + var params = { + group_id: 1 + } + App.Event.trigger('TextModulePreconditionUpdate', { taskKey: 'test1', params: params }) + + currentCollection = textModule.currentCollection(); + + equal(currentCollection.length, 2, 'trigered TextModulePreconditionUpdate with group_id') + equal(currentCollection[0].id, 1) + equal(currentCollection[1].id, 3) + + // trigered TextModulePreconditionUpdate with wrong group_id + + params = { + group_id: 3 + } + App.Event.trigger('TextModulePreconditionUpdate', { taskKey: 'test1', params: params }) + + currentCollection = textModule.currentCollection(); + + equal(currentCollection.length, 1, 'trigered TextModulePreconditionUpdate with wrong group_id') + equal(currentCollection[0].id, 1) + + // trigered TextModulePreconditionUpdate with group_id but wrong taskKey + + params = { + group_id: 3 + } + App.Event.trigger('TextModulePreconditionUpdate', { taskKey: 'test2', params: params }) + + currentCollection = textModule.currentCollection(); + + equal(currentCollection.length, 1, 'trigered TextModulePreconditionUpdate with group_id but wrong taskKey - nothing has changed') + equal(currentCollection[0].id, 1) + + // trigered TextModulePreconditionUpdate without group_id + + params = { + owner_id: 2 + } + App.Event.trigger('TextModulePreconditionUpdate', { taskKey: 'test1', params: params }) + + currentCollection = textModule.currentCollection(); + + equal(currentCollection.length, 2, 'trigered TextModulePreconditionUpdate without group_id') + equal(currentCollection[0].id, 1) + equal(currentCollection[1].id, 3) + +}); diff --git a/spec/factories/text_module.rb b/spec/factories/text_module.rb new file mode 100644 index 000000000..88d47afec --- /dev/null +++ b/spec/factories/text_module.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :text_module do + name { 'text module ' + Faker::Number.unique.number(3) } + keywords { Faker::Superhero.prefix } + content { Faker::Lorem.sentence } + updated_by_id { 1 } + created_by_id { 1 } + end +end diff --git a/spec/support/capybara/common_actions.rb b/spec/support/capybara/common_actions.rb index f9194a39b..60193e1e6 100644 --- a/spec/support/capybara/common_actions.rb +++ b/spec/support/capybara/common_actions.rb @@ -66,6 +66,25 @@ module CommonActions find('.user-menu .user a')[:title] end + # Returns the User record for the currently logged in user. + # + # @example + # current_user.login + # => 'master@example.com' + # + # @example + # current_user do |user| + # user.group_names_access_map = group_names_access_map + # user.save! + # end + # + # @return [User] the current user record. + def current_user + ::User.find_by(login: current_login).tap do |user| + yield user if block_given? + end + end + # Logs out the currently logged in user. # # @example diff --git a/spec/support/capybara/selectors.rb b/spec/support/capybara/selectors.rb index 58bb2fccb..0e2c7da51 100644 --- a/spec/support/capybara/selectors.rb +++ b/spec/support/capybara/selectors.rb @@ -17,5 +17,5 @@ Capybara.add_selector(:clues_close) do end Capybara.add_selector(:richtext) do - css { |name| "div[data-name=#{name}]" } + css { |name| "div[data-name=#{name || 'body'}]" } end diff --git a/spec/system/examples/text_modules_group_dependency_examples.rb b/spec/system/examples/text_modules_group_dependency_examples.rb new file mode 100644 index 000000000..3d716bcdb --- /dev/null +++ b/spec/system/examples/text_modules_group_dependency_examples.rb @@ -0,0 +1,51 @@ +RSpec.shared_examples 'group-dependent text modules' do |path:| + + let!(:group1) { create :group } + let!(:group2) { create :group } + let!(:text_module_without_group) { create :text_module } + let!(:text_module_group1) { create :text_module, groups: [group1] } + let!(:text_module_group2) { create :text_module, groups: [group2] } + + it 'supports group-dependent text modules' 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 + + visit path + + within(:active_content) do + + selector_group_select = 'select[name="group_id"]' + selector_text_module_selection = '.shortcut' + selector_text_module_item = ".shortcut > ul > li[data-id='%s']" + + # exercise + find(selector_group_select).find(:option, group1.name).select_option + find(:richtext).send_keys('::') + + # expectations + expect(page).to have_css(selector_text_module_selection, wait: 3) + expect(page).to have_css(format(selector_text_module_item, text_module_without_group.id)) + expect(page).to have_css(format(selector_text_module_item, text_module_group1.id)) + expect(page).to have_no_css(format(selector_text_module_item, text_module_group2.id)) + + # exercise + find(selector_group_select).find(:option, group2.name).select_option + find(:richtext).send_keys('::') + + # expectations + expect(page).to have_css(selector_text_module_selection, wait: 3) + expect(page).to have_css(format(selector_text_module_item, text_module_without_group.id)) + expect(page).to have_no_css(format(selector_text_module_item, text_module_group1.id)) + expect(page).to have_css(format(selector_text_module_item, text_module_group2.id)) + end + end +end diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb index 5a6c48295..f04bd1ed7 100644 --- a/spec/system/ticket/create_spec.rb +++ b/spec/system/ticket/create_spec.rb @@ -1,5 +1,7 @@ require 'rails_helper' +require 'system/examples/text_modules_group_dependency_examples' + RSpec.describe 'Ticket Create', type: :system do context 'when applying ticket templates' do # Regression test for issue #2424 - Unavailable ticket template attributes get applied @@ -30,4 +32,8 @@ RSpec.describe 'Ticket Create', type: :system do expect(page).not_to have_selector 'select[name="group_id"]' end end + + context 'when using text modules' do + include_examples 'group-dependent text modules', path: 'ticket/create' + end end diff --git a/spec/system/ticket/update_spec.rb b/spec/system/ticket/update_spec.rb index 9f664782c..6f8838c0a 100644 --- a/spec/system/ticket/update_spec.rb +++ b/spec/system/ticket/update_spec.rb @@ -1,5 +1,7 @@ require 'rails_helper' +require 'system/examples/text_modules_group_dependency_examples' + RSpec.describe 'Ticket Update', type: :system do let(:group) { Group.find_by(name: 'Users') } @@ -83,7 +85,7 @@ RSpec.describe 'Ticket Update', type: :system do }) # refresh browser to get macro accessable - page.driver.browser.navigate.refresh + refresh # create a new ticket and attempt to update its state without the required select attribute ticket = create(:ticket, group: group) @@ -135,4 +137,8 @@ RSpec.describe 'Ticket Update', type: :system do end end end + + context 'when using text modules' do + include_examples 'group-dependent text modules', path: "#ticket/zoom/#{Ticket.first.id}" + end end