diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99741dc47..d742f2338 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -127,6 +127,17 @@ test:integration:email_deliver: - ruby -I test/ test/integration/email_deliver_test.rb - rake db:drop +test:integration:email_keep_on_server: + stage: test + tags: + - core + script: + - export RAILS_ENV=test + - rake db:create + - rake db:migrate + - ruby -I test/ test/integration/email_keep_on_server_test.rb + - rake db:drop + test:integration:twitter: stage: test tags: @@ -332,7 +343,7 @@ test:integration:otrs_5_mysql: - mysql script: - export RAILS_ENV=test - - export IMPORT_OTRS_ENDPOINT="http://vz599.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" + - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" - rake db:create - rake db:migrate - ruby -I test/ test/integration/otrs_import_test.rb @@ -345,7 +356,7 @@ test:integration:otrs_5_postgresql: - postgresql script: - export RAILS_ENV=test - - export IMPORT_OTRS_ENDPOINT="http://vz599.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" + - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" - rake db:create - rake db:migrate - ruby -I test/ test/integration/otrs_import_test.rb diff --git a/.pkgr.yml b/.pkgr.yml index 85265c521..652c94518 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -5,19 +5,23 @@ notifications: false targets: centos-7: dependencies: + - elasticsearch - nginx - postgresql-server - which debian-8: dependencies: + - elasticsearch - nginx|apache2 - postgresql|mysql-server|mariadb-server|sqlite ubuntu-16.04: dependencies: + - elasticsearch - nginx|apache2 - postgresql|mysql-server|mariadb-server|sqlite sles-12: dependencies: + - elasticsearch - nginx - postgresql-server before: diff --git a/.ruby-version b/.ruby-version index 2bf1c1ccf..005119baa 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.1 +2.4.1 diff --git a/.travis.yml b/.travis.yml index 510217e74..e3cfaaa1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ notifications: env: - DB=mysql - DB=postgresql - - BUNDLE_JOBS=8 addons: postgresql: "9.4" apt: @@ -20,7 +19,7 @@ services: - mysql language: ruby rvm: - - 2.3.1 + - 2.4.1 before_install: - sudo apt-get -qq update - sudo apt-get install -y curl git-core patch build-essential bison zlib1g-dev libssl-dev libxml2-dev libxml2-dev sqlite3 libsqlite3-dev autotools-dev libxslt1-dev libyaml-0-2 autoconf automake libreadline6-dev libyaml-dev libtool libgmp-dev libgdbm-dev libncurses5-dev pkg-config libffi-dev libmysqlclient-dev postfix @@ -56,3 +55,4 @@ script: - ruby -I test/ test/integration/user_device_controller_test.rb - ruby -I test/ test/integration/sipgate_controller_test.rb - rake db:drop +after_success: contrib/travis-ci.org/trigger-docker-compose-build.sh diff --git a/Gemfile b/Gemfile index 2695e13bb..4d43388a0 100644 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' -ruby '2.3.1' +ruby '2.4.1' -gem 'rails', '4.2.8' +gem 'rails', '4.2.9' gem 'rails-observers' gem 'activerecord-session_store' @@ -40,6 +40,7 @@ gem 'omniauth-gitlab' gem 'omniauth-google-oauth2' gem 'omniauth-linkedin-oauth2' gem 'omniauth-twitter' +gem 'omniauth-microsoft-office365' gem 'twitter' gem 'telegramAPI' @@ -73,12 +74,16 @@ gem 'argon2' gem 'writeexcel' gem 'icalendar' +gem 'icalendar-recurrence' gem 'browser' # integrations gem 'slack-notifier' gem 'clearbit' gem 'zendesk_api' +gem 'viewpoint' +gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git' +gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git' # event machine gem 'eventmachine' diff --git a/Gemfile.lock b/Gemfile.lock index 5b161264b..96e3ada43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,44 +1,60 @@ +GIT + remote: https://github.com/thorsteneckel/autodiscover.git + revision: 29d713ee0c8c25fcf74c4292ff13fe1fa4d0d827 + specs: + autodiscover (1.0.2) + httpclient + logging + nokogiri + nori + +GIT + remote: https://github.com/wimm/rubyntlm.git + revision: 53969639b87b9e5d5fef560f19cf0d977259591c + specs: + rubyntlm (0.1.2) + GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) + actionmailer (4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.8) - actionview (= 4.2.8) - activesupport (= 4.2.8) + actionpack (4.2.9) + actionview (= 4.2.9) + activesupport (= 4.2.9) rack (~> 1.6) rack-test (~> 0.6.2) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.8) - activesupport (= 4.2.8) + actionview (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.8) - activesupport (= 4.2.8) + activejob (4.2.9) + activesupport (= 4.2.9) globalid (>= 0.3.0) - activemodel (4.2.8) - activesupport (= 4.2.8) + activemodel (4.2.9) + activesupport (= 4.2.9) builder (~> 3.1) - activerecord (4.2.8) - activemodel (= 4.2.8) - activesupport (= 4.2.8) + activerecord (4.2.9) + activemodel (= 4.2.9) + activesupport (= 4.2.9) arel (~> 6.0) - activerecord-nulldb-adapter (0.3.6) + activerecord-nulldb-adapter (0.3.7) activerecord (>= 2.0.0) - activerecord-session_store (1.0.0) - actionpack (>= 4.0, < 5.1) - activerecord (>= 4.0, < 5.1) + activerecord-session_store (1.1.0) + actionpack (>= 4.0, < 5.2) + activerecord (>= 4.0, < 5.2) multi_json (~> 1.11, >= 1.11.2) rack (>= 1.5.2, < 3) - railties (>= 4.0, < 5.1) - activesupport (4.2.8) + railties (>= 4.0, < 5.2) + activesupport (4.2.9) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) @@ -49,9 +65,9 @@ GEM ffi (~> 1.9) ffi-compiler (~> 0.1) ast (2.3.0) - autoprefixer-rails (6.4.1.1) + autoprefixer-rails (7.1.2.4) execjs - biz (1.6.0) + biz (1.7.0) clavius (~> 1.0) tzinfo browser (2.2.0) @@ -60,7 +76,7 @@ GEM childprocess (0.5.9) ffi (~> 1.0, >= 1.0.11) clavius (1.0.2) - clearbit (0.2.5) + clearbit (0.2.7) nestful (~> 1.1.0) coderay (1.1.1) coffee-rails (4.2.1) @@ -69,27 +85,27 @@ GEM coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.10.0) + coffee-script-source (1.12.2) coffeelint (1.14.0) coffee-script execjs json - composite_primary_keys (8.1.5) + composite_primary_keys (8.1.6) activerecord (~> 4.2.0) concurrent-ruby (1.0.5) - coveralls (0.8.16) + coveralls (0.8.21) json (>= 1.8, < 3) - simplecov (~> 0.12.0) - term-ansicolor (~> 1.3.0) - thor (~> 0.19.1) - tins (>= 1.6.0, < 2) + simplecov (~> 0.14.1) + term-ansicolor (~> 1.3) + thor (~> 0.19.4) + tins (~> 1.6) crack (0.4.3) safe_yaml (~> 1.0.0) daemons (1.2.4) - delayed_job (4.1.2) - activesupport (>= 3.0, < 5.1) - delayed_job_active_record (4.1.1) - activerecord (>= 3.0, < 5.1) + delayed_job (4.1.3) + activesupport (>= 3.0, < 5.2) + delayed_job_active_record (4.1.2) + activerecord (>= 3.0, < 5.2) delayed_job (>= 3.0, < 5) diff-lcs (1.2.5) diffy (3.1.0) @@ -151,10 +167,11 @@ GEM guard (~> 2.8) guard-compat (~> 1.0) multi_json (~> 1.8) - guard-symlink (0.1.0) + guard-symlink (0.1.1) + guard guard-compat (~> 1.1) - hashdiff (0.3.2) - hashie (3.4.4) + hashdiff (0.3.5) + hashie (3.5.6) htmlentities (4.3.4) http (1.0.4) addressable (~> 2.3) @@ -165,21 +182,30 @@ GEM domain_name (~> 0.5) http-form_data (1.0.3) http_parser.rb (0.6.0) - i18n (0.8.4) + httpclient (2.8.3) + i18n (0.8.6) icalendar (2.4.1) + icalendar-recurrence (1.1.2) + icalendar (~> 2.0) + ice_cube (~> 0.13) + ice_cube (0.16.2) inflection (1.0.0) json (1.8.6) - jwt (1.5.4) + jwt (1.5.6) kgio (2.11.0) koala (2.4.0) addressable faraday multi_json (>= 1.3.0) - libv8 (3.16.14.15) + libv8 (3.16.14.19) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) + little-plugger (1.1.4) + logging (2.2.2) + little-plugger (~> 1.1) + multi_json (~> 1.10) loofah (2.0.3) nokogiri (>= 1.5.9) lumberjack (1.0.10) @@ -192,9 +218,9 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_portile2 (2.2.0) - minitest (5.10.2) + minitest (5.10.3) multi_json (1.12.1) - multi_xml (0.5.5) + multi_xml (0.6.0) multipart-post (2.0.0) mysql2 (0.4.6) naught (1.1.0) @@ -204,37 +230,41 @@ GEM netrc (0.11.0) nokogiri (1.8.0) mini_portile2 (~> 2.2.0) + nori (2.6.0) notiffany (0.1.1) nenv (~> 0.1) shellany (~> 0.0) oauth (0.5.1) - oauth2 (1.2.0) - faraday (>= 0.8, < 0.10) + oauth2 (1.4.0) + faraday (>= 0.8, < 0.13) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) octokit (4.4.1) sawyer (~> 0.7.0, >= 0.5.3) - omniauth (1.3.1) - hashie (>= 1.2, < 4) - rack (>= 1.0, < 3) + omniauth (1.6.1) + hashie (>= 3.4.6, < 3.6.0) + rack (>= 1.6.2, < 3) omniauth-facebook (4.0.0) omniauth-oauth2 (~> 1.2) - omniauth-github (1.1.2) - omniauth (~> 1.0) - omniauth-oauth2 (~> 1.1) + omniauth-github (1.3.0) + omniauth (~> 1.5) + omniauth-oauth2 (>= 1.4.0, < 2.0) omniauth-gitlab (1.0.2) omniauth (~> 1.0) omniauth-oauth2 (~> 1.0) - omniauth-google-oauth2 (0.4.1) - jwt (~> 1.5.2) + omniauth-google-oauth2 (0.5.0) + jwt (~> 1.5) multi_json (~> 1.3) omniauth (>= 1.1.1) omniauth-oauth2 (>= 1.3.1) omniauth-linkedin-oauth2 (0.1.5) omniauth (~> 1.0) omniauth-oauth2 + omniauth-microsoft-office365 (0.0.7) + omniauth + omniauth-oauth2 omniauth-oauth (1.1.0) oauth omniauth (~> 1.0) @@ -246,32 +276,32 @@ GEM omniauth-oauth (~> 1.1) parser (2.3.1.2) ast (~> 2.2) - pg (0.18.4) - pluginator (1.3.0) + pg (0.20.0) + pluginator (1.5.0) power_assert (0.3.1) powerpack (0.1.1) - pre-commit (0.28.0) - pluginator (~> 1.1) + pre-commit (0.35.0) + pluginator (~> 1.5) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - puma (3.6.0) + puma (3.9.1) rack (1.6.8) rack-livereload (0.3.16) rack rack-test (0.6.3) rack (>= 1.0) - rails (4.2.8) - actionmailer (= 4.2.8) - actionpack (= 4.2.8) - actionview (= 4.2.8) - activejob (= 4.2.8) - activemodel (= 4.2.8) - activerecord (= 4.2.8) - activesupport (= 4.2.8) + rails (4.2.9) + actionmailer (= 4.2.9) + actionpack (= 4.2.9) + actionview (= 4.2.9) + activejob (= 4.2.9) + activemodel (= 4.2.9) + activerecord (= 4.2.9) + activesupport (= 4.2.9) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.8) + railties (= 4.2.9) sprockets-rails rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -281,15 +311,16 @@ GEM rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails-observers (0.1.2) - activemodel (~> 4.0) - railties (4.2.8) - actionpack (= 4.2.8) - activesupport (= 4.2.8) + rails-observers (0.1.5) + activemodel (>= 4.0) + railties (4.2.9) + actionpack (= 4.2.9) + activesupport (= 4.2.9) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rainbow (2.1.0) - raindrops (0.17.0) + rainbow (2.2.2) + rake + raindrops (0.19.0) rake (12.0.0) rb-fsevent (0.9.7) rb-inotify (0.9.7) @@ -337,7 +368,6 @@ GEM sawyer (0.7.0) addressable (>= 2.3.5, < 2.5) faraday (~> 0.8, < 0.10) - scrub_rb (1.0.1) selenium-webdriver (2.53.4) childprocess (~> 0.5) rubyzip (~> 1.0) @@ -345,16 +375,17 @@ GEM shellany (0.0.1) simple-rss (1.3.1) simple_oauth (0.3.1) - simplecov (0.12.0) + simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) - simplecov-html (0.10.0) + simplecov-html (0.10.1) simplecov-rcov (0.2.3) simplecov (>= 0.4.1) slack-notifier (1.5.1) slop (3.6.0) - spring (1.7.2) + spring (2.0.2) + activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) sprockets (3.7.1) @@ -367,17 +398,17 @@ GEM sqlite3 (1.3.11) telegramAPI (1.2.2) rest-client (~> 2.0, >= 1.7.3) - term-ansicolor (1.3.2) + term-ansicolor (1.6.0) tins (~> 1.0) test-unit (3.2.1) power_assert - therubyracer (0.12.2) - libv8 (~> 3.16.14.0) + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) ref thor (0.19.4) thread_safe (0.3.6) tilt (2.0.5) - tins (1.13.0) + tins (1.15.0) twitter (5.17.0) addressable (~> 2.3) buftok (~> 0.2.0) @@ -397,25 +428,29 @@ GEM unf_ext unf_ext (0.0.7.4) unicode-display_width (1.1.1) - unicorn (5.2.0) + unicorn (5.3.0) kgio (~> 2.6) raindrops (~> 0.7) - valid_email2 (1.2.17) + valid_email2 (2.0.0) activemodel (>= 3.2) mail (~> 2.5) - webmock (2.3.2) + viewpoint (1.1.0) + httpclient + logging + nokogiri + rubyntlm + webmock (3.0.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff websocket (1.2.3) writeexcel (1.0.5) - zendesk_api (1.14.0) + zendesk_api (1.14.4) faraday (~> 0.9) - hashie (>= 1.2, < 4.0, != 3.3.0) + hashie (>= 3.5.2, < 4.0.0) inflection mime-types multipart-post (~> 2.0) - scrub_rb (~> 1.0.1) PLATFORMS ruby @@ -424,6 +459,7 @@ DEPENDENCIES activerecord-nulldb-adapter activerecord-session_store argon2 + autodiscover! autoprefixer-rails biz browser @@ -449,6 +485,7 @@ DEPENDENCIES guard-symlink htmlentities icalendar + icalendar-recurrence json koala libv8 @@ -463,17 +500,19 @@ DEPENDENCIES omniauth-gitlab omniauth-google-oauth2 omniauth-linkedin-oauth2 + omniauth-microsoft-office365 omniauth-oauth2 omniauth-twitter pg pre-commit puma rack-livereload - rails (= 4.2.8) + rails (= 4.2.9) rails-observers rb-fsevent rspec-rails rubocop + rubyntlm! sass-rails selenium-webdriver simple-rss @@ -491,12 +530,13 @@ DEPENDENCIES uglifier unicorn valid_email2 + viewpoint webmock writeexcel zendesk_api RUBY VERSION - ruby 2.3.1p112 + ruby 2.4.1p111 BUNDLED WITH - 1.13.7 + 1.15.3 diff --git a/app/assets/javascripts/app/controllers/_application_controller.coffee b/app/assets/javascripts/app/controllers/_application_controller.coffee index 5952bb0bc..c6fc9fa3f 100644 --- a/app/assets/javascripts/app/controllers/_application_controller.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller.coffee @@ -61,13 +61,13 @@ class App.Controller extends Spine.Controller clearDelay: (delay_id) => App.Delay.clear(delay_id, @controllerId) - delay: (callback, timeout, delay_id, queue = true) => + delay: (callback, timeout, delay_id, queue = false) => App.Delay.set(callback, timeout, delay_id, @controllerId, queue) clearInterval: (interval_id) => App.Interval.clear(interval_id, @controllerId) - interval: (callback, interval, interval_id, queue = true) => + interval: (callback, interval, interval_id, queue = false) => App.Interval.set(callback, interval, interval_id, @controllerId, queue) releaseController: => @@ -185,6 +185,17 @@ class App.Controller extends Spine.Controller formValidate: (data) -> App.ControllerForm.validate(data) + # get all query params of the url + queryParam: -> + return if !@query + pairs = @query.split(';') + params = {} + for pair in pairs + result = pair.match('(.+?)=(.*)') + if result && result[1] + params[result[1]] = result[2] + params + # redirectToLogin: (data) -> # @@ -344,7 +355,10 @@ class App.Controller extends Spine.Controller title: -> userId = $(@).data('id') user = App.User.find(userId) - App.Utils.htmlEscape(user.displayName()) + headline = App.Utils.htmlEscape(user.displayName()) + if user.isOutOfOffice() + headline += " (#{App.Utils.htmlEscape(user.outOfOfficeText())})" + headline content: -> userId = $(@).data('id') user = App.User.fullLocal(userId) diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee index 6c8dfa547..f764e5c57 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee @@ -309,6 +309,23 @@ class App.ControllerConfirm extends App.ControllerModal if @callback @callback() +class App.ControllerErrorModal extends App.ControllerModal + buttonClose: true + buttonCancel: false + buttonSubmit: 'Close' + #buttonClass: 'btn--danger' + head: 'Error' + #small: true + #shown: true + + content: -> + @message + + onSubmit: => + @close() + if @callback + @callback() + class App.ControllerDrox extends App.Controller constructor: (params) -> super @@ -659,8 +676,9 @@ class App.Sidebar extends App.Controller render: => localEl = $(App.view('generic/sidebar_tabs')( - items: @items + items: @items scrollbarWidth: App.Utils.getScrollBarWidth() + dir: App.i18n.dir() )) # init content callback diff --git a/app/assets/javascripts/app/controllers/_channel/chat.coffee b/app/assets/javascripts/app/controllers/_channel/chat.coffee index a33ee73d7..a1cb273cb 100644 --- a/app/assets/javascripts/app/controllers/_channel/chat.coffee +++ b/app/assets/javascripts/app/controllers/_channel/chat.coffee @@ -21,7 +21,6 @@ class App.ChannelChat extends App.ControllerSubContent '.js-chat-welcome': 'chatWelcome' '.js-testurl-input': 'urlInput' '.js-backgroundColor': 'chatBackground' - '.js-paramsBlock': 'paramsBlock' '.js-code': 'code' '.js-palette': 'palette' '.js-color': 'colorField' @@ -361,7 +360,7 @@ class App.ChannelChat extends App.ControllerSubContent @$('.js-modal-params').html(paramString) # highlight - @paramsBlock.each (i, block) -> + @code.each (i, block) -> hljs.highlightBlock block App.Config.set('Chat', { prio: 4000, name: 'Chat', parent: '#channels', target: '#channels/chat', controller: App.ChannelChat, permission: ['admin.chat'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/_channel/email.coffee b/app/assets/javascripts/app/controllers/_channel/email.coffee index 99a71620e..5c92bc279 100644 --- a/app/assets/javascripts/app/controllers/_channel/email.coffee +++ b/app/assets/javascripts/app/controllers/_channel/email.coffee @@ -108,7 +108,7 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal # show errors in form if errors @log 'error', errors - @formValidate( form: e.target, errors: errors ) + @formValidate(form: e.target, errors: errors) return false # disable form @@ -118,8 +118,10 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal object.save( done: => @close() - fail: => - @close() + fail: (settings, details) => + @log 'errors', details + @formEnable(e) + @form.showAlert(details.error_human || details.error || 'Unable to create object!') ) class App.ChannelEmailSignature extends App.Controller @@ -201,7 +203,7 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal # show errors in form if errors @log 'error', errors - @formValidate( form: e.target, errors: errors ) + @formValidate(form: e.target, errors: errors) return false # disable form @@ -211,8 +213,10 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal object.save( done: => @close() - fail: => + fail: (settings, details) => + @log 'errors', details @formEnable(e) + @form.showAlert(details.error_human || details.error || 'Unable to create object!') ) class App.ChannelEmailAccountOverview extends App.Controller @@ -560,21 +564,24 @@ class App.ChannelEmailAccountWizard extends App.WizardModal # inbound configureAttributesInbound = [ - { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, - { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, - { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', }, - { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, - { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, - { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, - { name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false }, + { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, + { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, + { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off' }, + { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, + { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, + { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, + { name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' }, + { name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' }, ] showHideFolder = (params, attribute, attributes, classname, form, ui) -> return if !params if params.adapter is 'imap' ui.show('options::folder') + ui.show('options::keep_on_server') return ui.hide('options::folder') + ui.hide('options::keep_on_server') handlePort = (params, attribute, attributes, classname, form, ui) -> return if !params @@ -606,9 +613,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal # fill user / password based on intro info channel_used = { options: {} } if @account['meta'] - channel_used['options']['user'] = @account['meta']['email'] - channel_used['options']['password'] = @account['meta']['password'] - channel_used['options']['folder'] = @account['meta']['folder'] + channel_used['options']['user'] = @account['meta']['email'] + channel_used['options']['password'] = @account['meta']['password'] + channel_used['options']['folder'] = @account['meta']['folder'] + channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server'] # show used backend @$('.base-outbound-settings').html('') @@ -670,7 +678,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal for key, value of data.setting @account[key] = value - if data.content_messages && data.content_messages > 0 + if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) @$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro') @@ -724,7 +732,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal # remember account settings @account.inbound = params - if data.content_messages && data.content_messages > 0 + if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) @$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound') diff --git a/app/assets/javascripts/app/controllers/_channel/form.coffee b/app/assets/javascripts/app/controllers/_channel/form.coffee index 6955b3b60..7efd8168b 100644 --- a/app/assets/javascripts/app/controllers/_channel/form.coffee +++ b/app/assets/javascripts/app/controllers/_channel/form.coffee @@ -9,7 +9,7 @@ class App.ChannelForm extends App.ControllerSubContent 'change .js-paramsSetting select': 'updateGroup' elements: - '.js-paramsBlock': 'paramsBlock' + '.js-code': 'code' '.js-paramsSetting': 'paramsSetting' '.js-formSetting input': 'formSetting' @@ -43,7 +43,7 @@ class App.ChannelForm extends App.ControllerSubContent @html element - @paramsBlock.each (i, block) -> + @code.each (i, block) -> hljs.highlightBlock block @updateParamsDesigner() diff --git a/app/assets/javascripts/app/controllers/_integration/check_mk.coffee b/app/assets/javascripts/app/controllers/_integration/check_mk.coffee new file mode 100644 index 000000000..33c58ecf4 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/check_mk.coffee @@ -0,0 +1,46 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'check_mk_integration' + featureName: 'Check_MK' + featureConfig: 'check_mk_config' + description: [ + ['This service receives http requests from %s and creates tickets with host and service.', 'Check_MK'] + ['If the host and service is recovered again, the ticket will be closed automatically.'] + ] + + render: => + super + new App.SettingsForm( + area: 'Integration::CheckMK' + el: @$('.js-form') + ) + + new App.ScriptSnipped( + el: @$('.js-scriptSnipped') + facility: 'check_mk' + style: 'bash' + content: "#!/bin/bash\n\ncurl -X POST -F 'event_id=123' -F 'host=host1' -F 'service=http' -F 'state=down' #{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/integration/check_mk/#{App.Setting.get('check_mk_token')}" + description: [ + ['To enable %s for sending http requests to %s, you need create "%s" in the admin interface if %s.', 'Check_MK', 'Zammad', 'Event Actions', 'Check_MK'] + ] + ) + + new App.HttpLog( + el: @$('.js-log') + facility: 'check_mk' + ) + +class State + @current: -> + App.Setting.get('check_mk_integration') + +App.Config.set( + 'IntegrationCheckMk' + { + name: 'Check_MK' + target: '#system/integration/check_mk' + description: 'An open source monitoring tool.' + controller: Index + state: State + } + 'NavBarIntegrations' +) diff --git a/app/assets/javascripts/app/controllers/_integration/exchange.coffee b/app/assets/javascripts/app/controllers/_integration/exchange.coffee new file mode 100644 index 000000000..5e6cd1843 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/exchange.coffee @@ -0,0 +1,528 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'exchange_integration' + featureName: 'Exchange' + featureConfig: 'exchange_config' + description: [ + ['This service enables Zammad to connect with your Exchange server.'] + ] + events: + 'change .js-switch input': 'switch' + + render: => + super + new Form( + el: @$('.js-form') + ) + + #new App.ImportJob( + # el: @$('.js-importJob') + # facility: 'exchange' + #) + + new App.HttpLog( + el: @$('.js-log') + facility: 'exchange' + ) + + switch: => + super + active = @$('.js-switch input').prop('checked') + if active + job_start = => + @ajax( + id: 'jobs_config' + type: 'POST' + url: "#{@apiPath}/integration/exchange/job_start" + processData: true + success: (data, status, xhr) => + @render(true) + ) + + App.Delay.set( + job_start, + 600, + 'job_start', + ) + +class Form extends App.Controller + elements: + '.js-lastImport': 'lastImport' + '.js-wizard': 'wizardButton' + events: + 'click .js-wizard': 'startWizard' + 'click .js-start-sync': 'startSync' + + constructor: -> + super + @render() + @lastResult() + @activeDryRun() + + currentConfig: -> + App.Setting.get('exchange_config') || {} + + setConfig: (value) => + App.Setting.set('exchange_config', value, {notify: true}) + @startSync() + + render: (top = false) => + @config = @currentConfig() + + folders = [] + if !_.isEmpty(@config.folders) + for folder_id in @config.folders + folders.push @config.wizardData.backend_folders[folder_id] + + @html App.view('integration/exchange')( + config: @config, + folders: folders + ) + if _.isEmpty(@config) + @$('.js-notConfigured').removeClass('hide') + @$('.js-summary').addClass('hide') + else + @$('.js-notConfigured').addClass('hide') + @$('.js-summary').removeClass('hide') + + if top + a = => + @scrollToIfNeeded($('.content.active .page-header')) + @delay(a, 500) + + startSync: => + @ajax( + id: 'jobs_config' + type: 'POST' + url: "#{@apiPath}/integration/exchange/job_start" + processData: true + success: (data, status, xhr) => + @render(true) + @lastResult() + ) + + startWizard: (e) => + e.preventDefault() + new ConnectionWizard( + container: @el.closest('.content') + config: @config + callback: (config) => + @setConfig(config) + ) + + lastResult: => + @ajax( + id: 'jobs_start_index' + type: 'GET' + url: "#{@apiPath}/integration/exchange/job_start" + processData: true + success: (job, status, xhr) => + if !_.isEmpty(job) + if !@lastResultShowJob || @lastResultShowJob.updated_at != job.updated_at + @lastResultShowJob = job + @lastResultShow(job) + if job.finished_at + @wizardButton.attr('disabled', false) + else + @wizardButton.attr('disabled', true) + @delay(@lastResult, 5000) + ) + + lastResultShow: (job) => + if _.isEmpty(job) + @lastImport.html('') + return + countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped + job.result.failed + if !job.result.roles + job.result.roles = {} + for role_id, statistic of job.result.role_ids + role = App.Role.find(role_id) + job.result.roles[role.displayName()] = statistic + el = $(App.view('integration/exchange_last_import')(job: job, countDone: countDone)) + @lastImport.html(el) + + activeDryRun: => + @ajax( + id: 'jobs_try_index' + type: 'GET' + url: "#{@apiPath}/integration/exchange/job_try" + data: + finished: false + processData: true + success: (job, status, xhr) => + return if _.isEmpty(job) + + # show analyzing + new ConnectionWizard( + container: @el.closest('.content') + config: job.payload + start: 'tryLoop' + callback: (config) => + @wizardButton.attr('disabled', false) + @setConfig(config) + ) + @wizardButton.attr('disabled', true) + ) + +class State + @current: -> + App.Setting.get('exchange_integration') + +class ConnectionWizard extends App.WizardModal + wizardConfig: {} + slideMethod: + 'js-folders': 'foldersShow' + 'js-mapping': 'mappingShow' + + events: + 'submit form.js-discover': 'discover' + 'submit form.js-bind': 'folders' + 'submit form.js-folders': 'mapping' + 'click .js-mapping .js-submitTry': 'mappingChange' + 'click .js-try .js-submitSave': 'save' + 'click .js-close': 'hide' + 'click .js-remove': 'removeRow' + 'click .js-userMappingForm .js-add': 'addUserMapping' + 'click .js-goToSlide': 'goToSlide' + + elements: + '.modal-body': 'body' + '.js-foldersSelect': 'foldersSelect' + '.js-folders .js-submitTry': 'foldersSelectSubmit' + '.js-userMappingForm': 'userMappingForm' + '.js-expertForm': 'expertForm' + + constructor: -> + super + + if !_.isEmpty(@config) + @wizardConfig = @config + + if @container + @el.addClass('modal--local') + + @render() + + @el.modal + keyboard: true + show: true + backdrop: true + container: @container + .on + 'show.bs.modal': @onShow + 'shown.bs.modal': @onShown + 'hidden.bs.modal': => + @el.remove() + + if @slide + @showSlide(@slide) + else + @showDiscoverDetails() + + if @start + @[@start]() + + render: => + @html App.view('integration/exchange_wizard')() + + save: (e) => + e.preventDefault() + @callback(@wizardConfig) + @hide(e) + + showSlide: (slide) => + method = @slideMethod[slide] + if method && @[method] + @[method](true) + super + + showDiscoverDetails: => + @$('.js-discover input[name="user"]').val(@wizardConfig.user) + @$('.js-discover input[name="password"]').val(@wizardConfig.password) + + showBindDetails: => + @$('.js-bind input[name="endpoint"]').val(@wizardConfig.endpoint) + @$('.js-bind input[name="user"]').val(@wizardConfig.user) + @$('.js-bind input[name="password"]').val(@wizardConfig.password) + + discover: (e) => + e.preventDefault() + @showSlide('js-connect') + params = @formParam(e.target) + @ajax( + id: 'exchange_discover' + type: 'POST' + url: "#{@apiPath}/integration/exchange/autodiscover" + data: JSON.stringify(params) + processData: true + success: (data, status, xhr) => + if data.result isnt 'ok' + @showSlide('js-discover') + @showAlert('js-discover', data.message) + return + + @wizardConfig.endpoint = data.endpoint + @wizardConfig.user = params.user + @wizardConfig.password = params.password + + @showSlide('js-bind') + @showBindDetails() + + error: (xhr, statusText, error) => + detailsRaw = xhr.responseText + details = {} + if !_.isEmpty(detailsRaw) + details = JSON.parse(detailsRaw) + @showSlide('js-discover') + @showAlert('js-discover', details.error || 'Unable to perform backend.') + ) + + folders: (e) => + e.preventDefault() + @showSlide('js-analyze') + params = @formParam(e.target) + @ajax( + id: 'exchange_folders' + type: 'POST' + url: "#{@apiPath}/integration/exchange/folders" + data: JSON.stringify(params) + processData: true + success: (data, status, xhr) => + if data.result isnt 'ok' + @showSlide('js-bind') + @showAlert('js-bind', data.message) + return + + @wizardConfig.endpoint = params.endpoint + @wizardConfig.user = params.user + @wizardConfig.password = params.password + + # update wizard data + @wizardConfig.wizardData = {} + @wizardConfig.wizardData.backend_folders = data.folders + + @foldersShow() + + error: (xhr, statusText, error) => + detailsRaw = xhr.responseText + details = {} + if !_.isEmpty(detailsRaw) + details = JSON.parse(detailsRaw) + @showSlide('js-bind') + @showAlert('js-bind', details.error || 'Unable to perform backend.') + ) + + foldersShow: (alreadyShown) => + @showSlide('js-folders') if !alreadyShown + @foldersSelect.html(@createColumnSelection('folders', @wizardConfig.wizardData.backend_folders, @wizardConfig.folders)) + + createColumnSelection: (name, options, selected) -> + return App.UiElement.column_select.render( + name: name + null: false + nulloption: false + options: options + value: selected + onChange: (val) => + if val && val.length > 0 + @foldersSelectSubmit.removeClass('is-disabled') + else + @foldersSelectSubmit.addClass('is-disabled') + ) + + mapping: (e) => + e.preventDefault() + @showSlide('js-analyze') + params = @formParam(e.target) + + # folders might be a single selection so we + # have to ensure that is an Array so the + # backend and frontend can handle it properly + if typeof params.folders is 'string' + params.folders = [ params.folders ] + + # add login params + params.endpoint = @wizardConfig.endpoint + params.user = @wizardConfig.user + params.password = @wizardConfig.password + + @ajax( + id: 'exchange_mapping' + type: 'POST' + url: "#{@apiPath}/integration/exchange/mapping" + data: JSON.stringify(params) + processData: true + success: (data, status, xhr) => + if data.result isnt 'ok' + @showSlide('js-folders') + @showAlert('js-folders', data.message) + return + + attributes = {} + for key, value of App.User.attributesGet() + continue if key == 'login' + if (value.tag is 'input' || value.tag is 'richtext' || value.tag is 'textarea') && value.type isnt 'password' + attributes[key] = value.display || key + + @wizardConfig.wizardData.attributes = attributes + @wizardConfig.folders = params.folders + @wizardConfig.wizardData.backend_attributes = data.attributes + + @mappingShow() + + error: (xhr, statusText, error) => + detailsRaw = xhr.responseText + details = {} + if !_.isEmpty(detailsRaw) + details = JSON.parse(detailsRaw) + @showSlide('js-folders') + @showAlert('js-folders', details.error || 'Unable to perform backend.') + ) + + mappingShow: (alreadyShown) => + @showSlide('js-mapping') if !alreadyShown + user_attribute_map = @wizardConfig.attributes + + if _.isEmpty(user_attribute_map) + user_attribute_map = + given_name: 'firstname' + surname: 'lastname' + 'email_addresses.emailaddress1': 'email' + 'phone_numbers.businessphone': 'phone' + + @userMappingForm.find('tbody tr.js-entry').remove() + @userMappingForm.find('tbody tr').before(@buildRowsUserMap(user_attribute_map)) + + mappingChange: (e) => + e.preventDefault() + + # user map + attributes = @formParam(@userMappingForm) + for key in ['source', 'dest'] + if !_.isArray(attributes[key]) + attributes[key] = [attributes[key]] + attributes_local = + item_id: 'login' + length = attributes.source.length-1 + for count in [0..length] + if attributes.source[count] && attributes.dest[count] + attributes_local[attributes.source[count]] = attributes.dest[count] + @wizardConfig.attributes = attributes_local + + @tryShow() + + buildRowsUserMap: (user_attribute_map) => + + # show static login row + userUidDisplayValue = @wizardConfig.wizardData.backend_attributes['item_id'] + el = [ + $(App.view('integration/ldap_user_attribute_row_read_only')( + key: userUidDisplayValue, + value: 'Login' + )) + ] + + for source, dest of user_attribute_map + continue if source == 'item_id' + continue if !(source of @wizardConfig.wizardData.backend_attributes) + el.push @buildRowUserAttribute(source, dest) + el + + buildRowUserAttribute: (source, dest) => + el = $(App.view('integration/exchange_user_attribute_row')()) + el.find('.js-exchangeAttribute').html(@createSelection('source', @wizardConfig.wizardData.backend_attributes, source)) + el.find('.js-userAttribute').html(@createSelection('dest', @wizardConfig.wizardData.attributes, dest)) + el + + createSelection: (name, options, selected, unknown) -> + return App.UiElement.searchable_select.render( + name: name + multiple: false + limit: 100 + null: false + nulloption: false + options: options + value: selected + unknown: unknown + class: 'form-control--small' + ) + + removeRow: (e) -> + e.preventDefault() + $(e.target).closest('tr').remove() + + addUserMapping: (e) => + e.preventDefault() + @userMappingForm.find('tbody tr').last().before(@buildRowUserAttribute()) + + tryShow: (e) => + if e + e.preventDefault() + @showSlide('js-analyze') + + # create import job + @ajax( + id: 'exchange_try' + type: 'POST' + url: "#{@apiPath}/integration/exchange/job_try" + data: JSON.stringify(@wizardConfig) + processData: true + success: (data, status, xhr) => + @tryLoop() + ) + + tryLoop: => + @showSlide('js-dry') + @ajax( + id: 'jobs_try_index' + type: 'GET' + url: "#{@apiPath}/integration/exchange/job_try" + data: + finished: true + processData: true + success: (job, status, xhr) => + if job.result && (job.result.error || job.result.info) + @showSlide('js-error') + @showAlert('js-error', (job.result.error || job.result.info)) + return + + total = 0 + if job.result && _.keys(job.result).length > 0 + @$('.js-preprogress').addClass('hide') + @$('.js-analyzing').removeClass('hide') + + analized = 0 + total = job.result.sum + for action, sum of job.result + continue if action == 'folders' + continue if action == 'sum' + analized += sum + + @$('.js-progress progress').attr('value', analized) + @$('.js-progress progress').attr('max', total) + + if job.finished_at + # reset initial state in case the back button is used + @$('.js-preprogress').removeClass('hide') + @$('.js-analyzing').addClass('hide') + + @tryResult(job, total) + else + @delay(@tryLoop, 4000) + ) + + tryResult: (job, total) => + @showSlide('js-try') + el = $(App.view('integration/exchange_summary')(job: job, countDone: total)) + @el.find('.js-summary').html(el) + +App.Config.set( + 'IntegrationExchange' + { + name: 'Exchange' + target: '#system/integration/exchange' + description: 'Exchange integration for contacts management.' + controller: Index + state: State + } + 'NavBarIntegrations' +) diff --git a/app/assets/javascripts/app/controllers/_integration/idoit.coffee b/app/assets/javascripts/app/controllers/_integration/idoit.coffee new file mode 100644 index 000000000..724207069 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_integration/idoit.coffee @@ -0,0 +1,94 @@ +class Index extends App.ControllerIntegrationBase + featureIntegration: 'idoit_integration' + featureName: 'i-doit' + featureConfig: 'idoit_config' + description: [ + ['This service allows you to connect i-doit objects with Zammad.'] + ] + events: + 'change .js-switch input': 'switch' + + render: => + super + new Form( + el: @$('.js-form') + ) + + new App.HttpLog( + el: @$('.js-log') + facility: 'idoit' + ) + +class Form extends App.Controller + events: + 'submit form': 'update' + + constructor: -> + super + @render() + + currentConfig: -> + App.Setting.get('idoit_config') + + setConfig: (value) -> + App.Setting.set('idoit_config', value, {notify: true}) + + render: => + @config = @currentConfig() + + @html App.view('integration/idoit')( + config: @config + ) + + update: (e) => + e.preventDefault() + @config = @formParam(e.target) + @validateAndSave() + + validateAndSave: => + @ajax( + id: 'idoit' + type: 'POST' + url: "#{@apiPath}/integration/idoit/verify" + data: JSON.stringify( + method: 'cmdb.object_types' + api_token: @config.api_token + endpoint: @config.endpoint + client_id: @config.client_id + ) + success: (data, status, xhr) => + if data.result is 'failed' + new App.ControllerErrorModal( + message: data.message + container: @el.closest('.content') + ) + return + @setConfig(@config) + + error: (data, status) => + + # do not close window if request is aborted + return if status is 'abort' + + details = data.responseJSON || {} + @notify( + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to save!') + ) + ) + +class State + @current: -> + App.Setting.get('idoit_integration') + +App.Config.set( + 'IntegrationIdoit' + { + name: 'i-doit' + target: '#system/integration/idoit' + description: 'CMDB to document complex relations of your network components.' + controller: Index + state: State + } + 'NavBarIntegrations' +) diff --git a/app/assets/javascripts/app/controllers/_integration/ldap.coffee b/app/assets/javascripts/app/controllers/_integration/ldap.coffee index dc0bbbdef..83a61dc98 100644 --- a/app/assets/javascripts/app/controllers/_integration/ldap.coffee +++ b/app/assets/javascripts/app/controllers/_integration/ldap.coffee @@ -28,13 +28,20 @@ class Index extends App.ControllerIntegrationBase super active = @$('.js-switch input').prop('checked') if active - @ajax( - id: 'jobs_config' - type: 'POST' - url: "#{@apiPath}/integration/ldap/job_start" - processData: true - success: (data, status, xhr) => - @render(true) + job_start = => + @ajax( + id: 'jobs_config' + type: 'POST' + url: "#{@apiPath}/integration/ldap/job_start" + processData: true + success: (data, status, xhr) => + @render(true) + ) + + App.Delay.set( + job_start, + 600, + 'job_start', ) class Form extends App.Controller @@ -91,6 +98,7 @@ class Form extends App.Controller processData: true success: (data, status, xhr) => @render(true) + @lastResult() ) startWizard: (e) => diff --git a/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee b/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee index 671da7abc..5f731803b 100644 --- a/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee +++ b/app/assets/javascripts/app/controllers/_profile/linked_accounts.coffee @@ -9,43 +9,7 @@ class Index extends App.ControllerSubContent @render() render: => - auth_provider_all = { - facebook: { - url: '/auth/facebook' - name: 'Facebook' - config: 'auth_facebook' - }, - twitter: { - url: '/auth/twitter' - name: 'Twitter' - config: 'auth_twitter' - }, - linkedin: { - url: '/auth/linkedin' - name: 'LinkedIn' - config: 'auth_linkedin' - }, - github: { - url: '/auth/github' - name: 'GitHub' - config: 'auth_github' - }, - gitlab: { - url: '/auth/gitlab' - name: 'GitLab' - config: 'auth_gitlab' - }, - google_oauth2: { - url: '/auth/google_oauth2' - name: 'Google' - config: 'auth_google_oauth2' - }, - oauth2: { - url: '/auth/oauth2' - name: 'OAuth2' - config: 'auth_oauth2' - }, - } + auth_provider_all = App.Config.get('auth_provider_all') auth_providers = {} for key, provider of auth_provider_all if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true' @@ -90,3 +54,45 @@ class Index extends App.ControllerSubContent ) App.Config.set('LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index, permission: ['user_preferences.linked_accounts'] }, 'NavBarProfile') +App.Config.set('auth_provider_all', { + facebook: + url: '/auth/facebook' + name: 'Facebook' + config: 'auth_facebook' + class: 'facebook' + twitter: + url: '/auth/twitter' + name: 'Twitter' + config: 'auth_twitter' + class: 'twitter' + linkedin: + url: '/auth/linkedin' + name: 'LinkedIn' + config: 'auth_linkedin' + class: 'linkedin' + github: + url: '/auth/github' + name: 'GitHub' + config: 'auth_github' + class: 'github' + gitlab: + url: '/auth/gitlab' + name: 'GitLab' + config: 'auth_gitlab' + class: 'gitlab' + microsoft_office365: + url: '/auth/microsoft_office365' + name: 'Office 365' + config: 'auth_microsoft_office365' + class: 'office365' + google_oauth2: + url: '/auth/google_oauth2' + name: 'Google' + config: 'auth_google_oauth2' + class: 'google' + oauth2: + url: '/auth/oauth2' + name: 'OAuth2' + config: 'auth_oauth2' + class: 'oauth2' +}) diff --git a/app/assets/javascripts/app/controllers/_profile/out_of_office.coffee b/app/assets/javascripts/app/controllers/_profile/out_of_office.coffee new file mode 100644 index 000000000..8f796f715 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_profile/out_of_office.coffee @@ -0,0 +1,164 @@ +class Index extends App.ControllerSubContent + requiredPermission: 'user_preferences.out_of_office+ticket.agent' + header: 'Out of Office' + events: + 'submit form': 'submit' + 'click .js-disabled': 'disable' + 'click .js-enable': 'enable' + + constructor: -> + super + @render() + + render: => + user = @Session.get() + if !@localData + @localData = + out_of_office: user.out_of_office + out_of_office_start_at: user.out_of_office_start_at + out_of_office_end_at: user.out_of_office_end_at + out_of_office_replacement_id: user.out_of_office_replacement_id + out_of_office_replacement_id_completion: user.preferences.out_of_office_replacement_id_completion + out_of_office_text: user.preferences.out_of_office_text + form = $(App.view('profile/out_of_office')( + user: user + localData: @localData + placeholder: App.User.outOfOfficeTextPlaceholder() + )) + + dateStart = new App.ControllerForm( + model: + configure_attributes: + [ + name: 'out_of_office_start_at' + display: '' + tag: 'date' + past: false + future: true + null: false + ] + noFieldset: true + params: @localData + ) + form.find('.js-startDate').html(dateStart.form) + + dateEnd = new App.ControllerForm( + model: + configure_attributes: + [ + name: 'out_of_office_end_at' + display: '' + tag: 'date' + past: false + future: true + null: false + ] + noFieldset: true + params: @localData + ) + form.find('.js-endDate').html(dateEnd.form) + + agentList = new App.ControllerForm( + model: + configure_attributes: + [ + name: 'out_of_office_replacement_id' + display: '' + relation: 'User' + tag: 'user_autocompletion' + autocapitalize: false + multiple: false + limit: 30 + minLengt: 2 + placeholder: 'Enter Person or Organization/Company' + null: false + translate: false + disableCreateObject: true + value: @localData + ] + noFieldset: true + params: @localData + ) + form.find('.js-recipientDropdown').html(agentList.form) + if @localData.out_of_office is true + form.find('.js-disabled').removeClass('is-disabled') + #form.find('.js-enable').addClass('is-disabled') + else + form.find('.js-disabled').addClass('is-disabled') + #form.find('.js-enable').removeClass('is-disabled') + @html(form) + + enable: (e) => + e.preventDefault() + params = @formParam(e.target) + params.out_of_office = true + @store(e, params) + + disable: (e) => + e.preventDefault() + params = @formParam(e.target) + params.out_of_office = false + @store(e, params) + + submit: (e, params) => + e.preventDefault() + params = @formParam(e.target) + @store(e, params) + + store: (e, params) => + @formDisable(e) + for key, value of params + @localData[key] = value + App.Ajax.request( + id: 'user_out_of_office' + type: 'PUT' + url: "#{@apiPath}/users/out_of_office" + data: JSON.stringify(params) + processData: true + success: @success + error: @error + ) + + success: (data) => + if data.message is 'ok' + @render() + @notify( + type: 'success' + msg: App.i18n.translateContent('Successfully!') + timeout: 1000 + ) + else + if data.notice + @notify + type: 'error' + msg: App.i18n.translateContent(data.notice[0], data.notice[1]) + removeAll: true + else + @notify + type: 'error' + msg: 'Please contact your administrator.' + removeAll: true + @formEnable( @$('form') ) + + error: (xhr, status, error) => + @formEnable( @$('form') ) + + # do not close window if request is aborted + return if status is 'abort' + data = JSON.parse(xhr.responseText) + + # show error message + if xhr.status is 401 || error is 'Unauthorized' + message = '» ' + App.i18n.translateInline('Unauthorized') + ' «' + else if xhr.status is 404 || error is 'Not Found' + message = '» ' + App.i18n.translateInline('Not Found') + ' «' + else if data.error + message = App.i18n.translateInline(data.error) + else + message = '» ' + App.i18n.translateInline('Error') + ' «' + @notify + type: 'error' + msg: App.i18n.translateContent(message) + removeAll: true + +App.Config.set('OutOfOffice', { prio: 2800, name: 'Out of Office', parent: '#profile', target: '#profile/out_of_office', permission: ['user_preferences.out_of_office+ticket.agent'], controller: Index }, 'NavBarProfile') diff --git a/app/assets/javascripts/app/controllers/_settings/area_proxy.coffee b/app/assets/javascripts/app/controllers/_settings/area_proxy.coffee index 841e23ffc..8d093a776 100644 --- a/app/assets/javascripts/app/controllers/_settings/area_proxy.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area_proxy.coffee @@ -2,7 +2,7 @@ class App.SettingsAreaProxy extends App.Controller events: 'submit form': 'update' 'click .js-submit': 'update' - 'click .js-test': 'test2' + 'click .js-test': 'testConnection' constructor: -> super @@ -14,20 +14,21 @@ class App.SettingsAreaProxy extends App.Controller proxy: App.Setting.get('proxy') proxy_username: App.Setting.get('proxy_username') proxy_password: App.Setting.get('proxy_password') + proxy_no: App.Setting.get('proxy_no') ) update: (e) => e.preventDefault() @formDisable(e) params = @formParam(e) - console.log('params', params) App.Setting.set('proxy', params.proxy) App.Setting.set('proxy_username', params.proxy_username) App.Setting.set('proxy_password', params.proxy_password) + App.Setting.set('proxy_no', params.proxy_no) @formEnable(e) @render() - test2: (e) => + testConnection: (e) => e.preventDefault() params = @formParam(e) @ajax( diff --git a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee index 0ac160b3c..80d61840b 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee @@ -82,7 +82,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi view: shown: true invite_customer: - show: false + shown: false required: false 'admin.user': create: @@ -94,10 +94,10 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi view: shown: true invite_agent: - show: false + shown: false required: false invite_customer: - show: false + shown: false required: false Organization: 'ticket.customer': diff --git a/app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee b/app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee index 176030f3f..049bc2735 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee @@ -20,7 +20,7 @@ class App.UiElement.postmaster_set name: 'Customer' relation: 'User' tag: 'user_autocompletion' - disableCreateUser: true + disableCreateObject: true } { value: 'group_id' @@ -32,7 +32,7 @@ class App.UiElement.postmaster_set name: 'Owner' relation: 'User' tag: 'user_autocompletion' - disableCreateUser: true + disableCreateObject: true } ] article: diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee index e64d6edf4..c5718de48 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee @@ -33,14 +33,15 @@ class App.UiElement.ticket_perform_action elements["#{groupKey}.#{config.name}"] = config # add ticket deletion action - elements['ticket.action'] = - name: 'action' - display: 'Action' - tag: 'select' - null: false - translate: true - options: - delete: 'delete' + if attribute.ticket_delete + elements['ticket.action'] = + name: 'action' + display: 'Action' + tag: 'select' + null: false + translate: true + options: + delete: 'Delete' [defaults, groups, elements] diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee index 72ff6e1b8..91b5ad0b3 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee @@ -156,7 +156,7 @@ class App.UiElement.ticket_selector elementRow = $(e.target).closest('.js-filterElement') groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value') return if !groupAndAttribute - @buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute, false) + @buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute) ) # bind for preview @@ -244,9 +244,9 @@ class App.UiElement.ticket_selector if groupAndAttribute elementRow.find('.js-attributeSelector select').val(groupAndAttribute) - @buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute, true) + @buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) - @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute, buildValue) -> + @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) -> currentOperator = elementRow.find('.js-operator option:selected').attr('value') name = "#{attribute.name}::#{groupAndAttribute}::operator" @@ -284,9 +284,9 @@ class App.UiElement.ticket_selector elementRow.find('.js-operator select').replaceWith(selection) - @buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute, buildValue) + @buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) - @buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig, buildValue = true) -> + @buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) -> currentOperator = elementRow.find('.js-operator option:selected').attr('value') currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value') @@ -318,7 +318,6 @@ class App.UiElement.ticket_selector if !preCondition elementRow.find('.js-preCondition select').html('') elementRow.find('.js-preCondition').addClass('hide') - return if !buildValue toggleValue() @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) return @@ -351,7 +350,6 @@ class App.UiElement.ticket_selector toggleValue() ) - return if !buildValue @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) toggleValue() diff --git a/app/assets/javascripts/app/controllers/_ui_element/user_autocompletion_search.coffee b/app/assets/javascripts/app/controllers/_ui_element/user_autocompletion_search.coffee index b8d47b42f..f6dfae836 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/user_autocompletion_search.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/user_autocompletion_search.coffee @@ -2,5 +2,5 @@ class App.UiElement.user_autocompletion_search @render: (attributeOrig, params = {}) -> attribute = _.clone(attributeOrig) - attribute.disableCreateUser = true + attribute.disableCreateObject = true new App.UserOrganizationAutocompletion(attribute: attribute, params: params).element() diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index 38b2228ca..617c83f0c 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -9,6 +9,7 @@ class App.TicketCreate extends App.Controller constructor: (params) -> super + @sidebarState = {} # define default type @default_type = 'phone-in' @@ -91,6 +92,8 @@ class App.TicketCreate extends App.Controller else @$('[name="cc"]').closest('.form-group').addClass('hide') + App.TaskManager.touch(@task_key) + meta: => text = '' if @articleAttributes @@ -99,10 +102,10 @@ class App.TicketCreate extends App.Controller if title text = "#{text}: #{title}" meta = - url: @url() - head: text - title: text - id: @id + url: @url() + head: text + title: text + id: @id iconClass: 'pen' url: => @@ -228,7 +231,7 @@ class App.TicketCreate extends App.Controller type = @$('[name="formSenderType"]').val() if signature isnt undefined && signature.body && type is 'email-out' - signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get() }) + signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), config: App.Config.all() }) body = @$('[data-name=body]') if App.Utils.signatureCheck(body.html() || '', signatureFinished) @@ -330,27 +333,47 @@ class App.TicketCreate extends App.Controller # show text module UI @textModule = new App.WidgetTextModule( el: @$('[data-name="body"]').parent() - ) - - new Sidebar( - el: @sidebar - params: @formDefault - textModule: @textModule + data: + config: App.Config.all() + user: App.Session.get() ) $('#tags').tokenfield() + @sidebarWidget = new App.TicketCreateSidebar( + el: @sidebar + params: @formDefault + sidebarState: @sidebarState + task_key: @task_key + query: @query + ) + + if @formDefault.customer_id + callback = (customer) => + @localUserInfoCallback(@formDefault, customer) + App.User.full(@formDefault.customer_id, callback) + # update taskbar with new meta data App.TaskManager.touch(@task_key) localUserInfo: (e) => - + return if !@sidebarWidget params = App.ControllerForm.params($(e.target).closest('form')) - new Sidebar( - el: @sidebar - params: params - textModule: @textModule + if params.customer_id + callback = (customer) => + @localUserInfoCallback(params, customer) + App.User.full(params.customer_id, callback) + return + @localUserInfoCallback(params) + + localUserInfoCallback: (params, customer = {}) => + @sidebarWidget.render(params) + @textModule.reload( + config: App.Config.all() + user: App.Session.get() + ticket: + customer: customer ) cancel: (e) -> @@ -475,6 +498,10 @@ class App.TicketCreate extends App.Controller # scroll to top ui.scrollTo() + # add sidebar params + if ui.sidebarWidget + ui.sidebarWidget.commit(ticket_id: @id) + # access to group for group_id, access of App.Session.get('group_ids') if @group_id.toString() is group_id.toString() @@ -495,114 +522,6 @@ class App.TicketCreate extends App.Controller ) ) -class Sidebar extends App.Controller - constructor: -> - super - - # load user - if @params['customer_id'] - App.User.full(@params['customer_id'], @render) - return - - # render ui - @render() - - render: (user) => - - items = [] - if user - - showCustomer = (el) => - # update text module UI - if @textModule - @textModule.reload( - ticket: - customer: user - user: App.Session.get() - ) - - new App.WidgetUser( - el: el - user_id: user.id - ) - - editCustomer = (e, el) => - new App.ControllerGenericEdit( - id: @params.customer_id - genericObject: 'User' - screen: 'edit' - pageData: - title: 'Users' - object: 'User' - objects: 'Users' - container: @el.closest('.content') - ) - items.push { - head: 'Customer' - name: 'customer' - icon: 'person' - actions: [ - { - title: 'Edit Customer' - name: 'Edit Customer' - class: 'glyphicon glyphicon-edit' - callback: editCustomer - }, - ] - callback: showCustomer - } - - if user.organization_id - editOrganization = (e, el) => - new App.ControllerGenericEdit( - id: user.organization_id - genericObject: 'Organization' - pageData: - title: 'Organizations' - object: 'Organization' - objects: 'Organizations' - container: @el.closest('.content') - ) - showOrganization = (el) -> - new App.WidgetOrganization( - el: el - organization_id: user.organization_id - ) - items.push { - head: 'Organization' - name: 'organization' - icon: 'group' - actions: [ - { - title: 'Edit Organization' - name: 'Edit Organization' - class: 'glyphicon glyphicon-edit' - callback: editOrganization - }, - ] - callback: showOrganization - } - - showTemplates = (el) -> - - # show template UI - new App.WidgetTemplate( - el: el - #template_id: template['id'] - ) - - items.push { - head: 'Templates' - name: 'template' - icon: 'templates' - callback: showTemplates - } - - new App.Sidebar( - el: @el - items: items - ) - class Router extends App.ControllerPermanent requiredPermission: 'ticket.agent' constructor: (params) -> @@ -618,6 +537,9 @@ class Router extends App.ControllerPermanent if params.customer_id split = "/customer/#{params.customer_id}" + if params.query + split = "/query/#{params.query}" + id = Math.floor( Math.random() * 99999 ) @navigate "#ticket/create/id/#{id}#{split}" return @@ -628,6 +550,7 @@ class Router extends App.ControllerPermanent article_id: params.article_id type: params.type customer_id: params.customer_id + query: params.query id: params.id App.TaskManager.execute( @@ -643,6 +566,7 @@ App.Config.set('ticket/create/', Router, 'Routes') App.Config.set('ticket/create/id/:id', Router, 'Routes') App.Config.set('ticket/create/customer/:customer_id', Router, 'Routes') App.Config.set('ticket/create/id/:id/customer/:customer_id', Router, 'Routes') +App.Config.set('ticket/create/id/:id/query/:query', Router, 'Routes') # split ticket App.Config.set('ticket/create/:ticket_id/:article_id', Router, 'Routes') diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar.coffee new file mode 100644 index 000000000..88b151449 --- /dev/null +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar.coffee @@ -0,0 +1,43 @@ +class App.TicketCreateSidebar extends App.Controller + constructor: -> + super + @render() + + reload: (args) => + for key, backend of @sidebarBackends + if backend && backend.reload + backend.reload(args) + + commit: (args) => + for key, backend of @sidebarBackends + if backend && backend.commit + backend.commit(args) + + render: (params) => + if params + @params = params + @sidebarBackends ||= {} + @sidebarItems = [] + sidebarBackends = App.Config.get('TicketCreateSidebar') + keys = _.keys(sidebarBackends).sort() + for key in keys + if !@sidebarBackends[key] || !@sidebarBackends[key].reload + @sidebarBackends[key] = new sidebarBackends[key]( + params: @params + query: @query + taskGet: @taskGet + ) + else + @sidebarBackends[key].reload( + params: @params + query: @query + ) + item = @sidebarBackends[key].sidebarItem() + if item + @sidebarItems.push item + + new App.Sidebar( + el: @el + sidebarState: @sidebarState + items: @sidebarItems + ) diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee new file mode 100644 index 000000000..cbfd4e7e9 --- /dev/null +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee @@ -0,0 +1,38 @@ +class SidebarCustomer extends App.Controller + sidebarItem: => + return if !@permissionCheck('ticket.agent') + return if !@params.customer_id + { + head: 'Customer' + name: 'customer' + icon: 'person' + actions: [ + { + title: 'Edit Customer' + name: 'customer-edit' + callback: @editCustomer + }, + ] + callback: @showCustomer + } + + showCustomer: (el) => + @el = el + new App.WidgetUser( + el: @el + user_id: @params.customer_id + ) + + editCustomer: => + new App.ControllerGenericEdit( + id: @params.customer_id + genericObject: 'User' + screen: 'edit' + pageData: + title: 'Users' + object: 'User' + objects: 'Users' + container: @el.closest('.content') + ) + +App.Config.set('200-Customer', SidebarCustomer, 'TicketCreateSidebar') diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_organization.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_organization.coffee new file mode 100644 index 000000000..db2a9239d --- /dev/null +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_organization.coffee @@ -0,0 +1,41 @@ +class SidebarOrganization extends App.Controller + sidebarItem: => + return if !@permissionCheck('ticket.agent') + return if !@params.customer_id + return if !App.User.exists(@params.customer_id) + customer = App.User.find(@params.customer_id) + @organization_id = customer.organization_id + return if !@organization_id + { + head: 'Organization' + name: 'organization' + icon: 'group' + actions: [ + { + title: 'Edit Organization' + name: 'organization-edit' + callback: @editOrganization + }, + ] + callback: @showOrganization + } + + showOrganization: (el) => + @el = el + new App.WidgetOrganization( + el: @el + organization_id: @organization_id + ) + + editOrganization: => + new App.ControllerGenericEdit( + id: @organization_id, + genericObject: 'Organization' + pageData: + title: 'Organizations' + object: 'Organization' + objects: 'Organizations' + container: @el.closest('.content') + ) + +App.Config.set('300-Organization', SidebarOrganization, 'TicketCreateSidebar') diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_template.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_template.coffee new file mode 100644 index 000000000..bb724bc83 --- /dev/null +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_template.coffee @@ -0,0 +1,21 @@ +class SidebarTemplate extends App.Controller + sidebarItem: => + return if !@permissionCheck('ticket.agent') + { + head: 'Templates' + name: 'template' + icon: 'templates' + actions: [] + callback: @showTemplates + } + + showTemplates: (el) => + @el = el + + # show template UI + new App.WidgetTemplate( + el: el + #template_id: template['id'] + ) + +App.Config.set('100-Template', SidebarTemplate, 'TicketCreateSidebar') diff --git a/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee b/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee index aad33ceb4..d71087198 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_merge.coffee @@ -98,9 +98,13 @@ class App.TicketMerge extends App.ControllerModal type: 'error' msg: App.i18n.translateContent(data['message']) timeout: 6000 - @formEnable(e) - error: => + error: (data) => + details = data.responseJSON || {} + @notify + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to merge!') + timeout: 6000 @formEnable(e) ) diff --git a/app/assets/javascripts/app/controllers/chat.coffee b/app/assets/javascripts/app/controllers/chat.coffee index f8e969e86..6023b6a8a 100644 --- a/app/assets/javascripts/app/controllers/chat.coffee +++ b/app/assets/javascripts/app/controllers/chat.coffee @@ -462,6 +462,14 @@ class ChatWindow extends App.Controller ) ) + # show text module UI + new App.WidgetTextModule( + el: @input + data: + user: App.Session.get() + config: App.Config.all() + ) + focus: => @input.focus() @@ -473,7 +481,7 @@ class ChatWindow extends App.Controller if event.data and event.data.callback event.data.callback() - @$('.js-customerChatInput').ce({ + @input.ce({ mode: 'richtext' multiline: true maxlength: 40000 @@ -522,7 +530,7 @@ class ChatWindow extends App.Controller switch event.keyCode when TABKEY - allChatInputs = $('.js-customerChatInput').not('[disabled="disabled"]') + allChatInputs = @input.not('[disabled="disabled"]') chatCount = allChatInputs.size() index = allChatInputs.index(@input) @@ -542,7 +550,7 @@ class ChatWindow extends App.Controller allChatInputs.eq(chatCount-1).focus() when ENTERKEY - if !event.shiftKey + if !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey event.preventDefault() @sendMessage() @@ -587,7 +595,7 @@ class ChatWindow extends App.Controller @sounds.message.play() @notifyDesktop( title: @name - body: message + body: App.Utils.html2text(message) url: '#customer_chat' callback: => App.Event.trigger('chat_focus', { session_id: @session.session_id }) diff --git a/app/assets/javascripts/app/controllers/getting_started.coffee b/app/assets/javascripts/app/controllers/getting_started.coffee index c7d236267..8c2fbb71d 100644 --- a/app/assets/javascripts/app/controllers/getting_started.coffee +++ b/app/assets/javascripts/app/controllers/getting_started.coffee @@ -450,8 +450,8 @@ class EmailNotification extends App.WizardFullScreen if adapter is 'smtp' configureAttributesOutbound = [ { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, - { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password' }, - { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, + { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off' }, + { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, ] @form = new App.ControllerForm( @@ -671,20 +671,24 @@ class ChannelEmail extends App.WizardFullScreen # inbound configureAttributesInbound = [ - { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, - { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, - { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', }, - { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, - { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, - { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, + { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, + { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, + { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', }, + { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', single: true }, + { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, + { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, + { name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' }, + { name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' }, ] showHideFolder = (params, attribute, attributes, classname, form, ui) -> return if !params if params.adapter is 'imap' ui.show('options::folder') + ui.show('options::keep_on_server') return ui.hide('options::folder') + ui.hide('options::keep_on_server') handlePort = (params, attribute, attributes, classname, form, ui) -> return if !params @@ -700,7 +704,7 @@ class ChannelEmail extends App.WizardFullScreen return new App.ControllerForm( - el: @$('.base-inbound-settings'), + el: @$('.base-inbound-settings') model: configure_attributes: configureAttributesInbound className: '' @@ -716,8 +720,10 @@ class ChannelEmail extends App.WizardFullScreen # fill user / password based on intro info channel_used = { options: {} } if @account['meta'] - channel_used['options']['user'] = @account['meta']['email'] - channel_used['options']['password'] = @account['meta']['password'] + channel_used['options']['user'] = @account['meta']['email'] + channel_used['options']['password'] = @account['meta']['password'] + channel_used['options']['folder'] = @account['meta']['folder'] + channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server'] # show used backend @$('.base-outbound-settings').html('') @@ -725,8 +731,8 @@ class ChannelEmail extends App.WizardFullScreen if adapter is 'smtp' configureAttributesOutbound = [ { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, - { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', }, - { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, + { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', }, + { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, ] @form = new App.ControllerForm( @@ -745,7 +751,7 @@ class ChannelEmail extends App.WizardFullScreen @account.meta = params @disable(e) - @$('.js-probe .js-email').text( params.email ) + @$('.js-probe .js-email').text(params.email) @showSlide('js-probe') @ajax( @@ -760,7 +766,7 @@ class ChannelEmail extends App.WizardFullScreen for key, value of data.setting @account[key] = value - if data.content_messages && data.content_messages > 0 + if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) @$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro') @@ -809,7 +815,7 @@ class ChannelEmail extends App.WizardFullScreen # remember account settings @account.inbound = params - if data.content_messages && data.content_messages > 0 + if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true) message = App.i18n.translateContent('We have already found %s emails in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) @$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound') diff --git a/app/assets/javascripts/app/controllers/idoit_object_selector.coffee b/app/assets/javascripts/app/controllers/idoit_object_selector.coffee new file mode 100644 index 000000000..d2b7f2065 --- /dev/null +++ b/app/assets/javascripts/app/controllers/idoit_object_selector.coffee @@ -0,0 +1,100 @@ +class App.IdoitObjectSelector extends App.ControllerModal + buttonClose: true + buttonCancel: true + buttonSubmit: true + head: 'i-doit' + + content: -> + @ajax( + id: 'idoit-object-selector' + type: 'POST' + url: "#{@apiPath}/integration/idoit" + data: JSON.stringify(method: 'cmdb.object_types') + success: (data, status, xhr) => + if data.result is 'failed' + @contentInline = data.message + @render() + return + + result = _.sortBy(data.response.result, 'title') + @contentInline = $(App.view('integration/idoit_object_selector')()) + + @contentInline.find('.js-typeSelect').html(@renderTypeSelector(result)) + + @contentInline.filter('.js-search').on('change', 'select, input', (e) => + params = @formParam(e.target) + @search(params) + ) + @contentInline.filter('.js-search').on('keyup', 'input', (e) => + params = @formParam(e.target) + @search(params) + ) + @render() + @$('.js-input').focus() + + error: (xhr, status, error) => + + # do not close window if request is aborted + return if status is 'abort' + + # show error message + @contentInline = 'Unable to load content' + @render() + ) + '' + + search: (filter) => + if _.isEmpty(filter.title) + delete filter.title + else + filter.title = "%#{filter.title}%" + @ajax( + id: 'idoit-object-selector' + type: 'POST' + url: "#{@apiPath}/integration/idoit" + data: JSON.stringify(method: 'cmdb.objects', filter: filter) + success: (data, status, xhr) => + @renderResult(data.response.result) + + error: (xhr, status, error) => + + # do not close window if request is aborted + return if status is 'abort' + + # show error message + @contentInline = 'Unable to load content' + @render() + ) + + renderResult: (items) => + table = App.view('integration/idoit_object_result')( + items: items + ) + @el.find('.js-result').html(table) + + renderTypeSelector: (result) -> + options = {} + for item in result + options[item.id] = item.title + return App.UiElement.searchable_select.render( + name: 'type' + multiple: false + limit: 100 + null: false + nulloption: false + options: options + ) + + onSubmit: (e) => + form = @el.find('.js-result') + params = @formParam(form) + return if _.isEmpty(params.object_id) + + if _.isArray(params.object_id) + object_ids = params.object_id + else + object_ids = [params.object_id] + + @formDisable(form) + @callback(object_ids, @) + diff --git a/app/assets/javascripts/app/controllers/import_zendesk.coffee b/app/assets/javascripts/app/controllers/import_zendesk.coffee index 04ffeee3b..1858adde1 100644 --- a/app/assets/javascripts/app/controllers/import_zendesk.coffee +++ b/app/assets/javascripts/app/controllers/import_zendesk.coffee @@ -10,6 +10,7 @@ class Index extends App.ControllerContent '.zendesk-api-token-error': 'apiTokenErrorMessage' '#zendesk-email': 'zendeskEmail' '#zendesk-api-token': 'zendeskApiToken' + '.js-ticket-count-info': 'ticketCountInfo' updateMigrationDisplayLoop: 0 events: @@ -116,7 +117,8 @@ class Index extends App.ControllerContent showCredentials: (e) => e.preventDefault() @urlStatus.attr('data-state', '') - @zendeskUrlApiToken.attr('href', @zendeskUrl.val() + 'agent/admin/api') + url = @zendeskUrl.val() + '/agent/admin/api' + @zendeskUrlApiToken.attr('href', url.replace(/([^:])\/\/+/g, '$1/')) @zendeskUrlApiToken.val('HERE') @$('[data-slide=zendesk-url]').toggleClass('hide') @$('[data-slide=zendesk-credentials]').toggleClass('hide') @@ -171,6 +173,10 @@ class Index extends App.ControllerContent for key, item of data.data if item.done > item.total item.done = item.total + + if key == 'Ticket' && item.total >= 1000 + @ticketCountInfo.removeClass('hide') + element = @$('.js-' + key.toLowerCase() ) element.find('.js-done').text(item.done) element.find('.js-total').text(item.total) diff --git a/app/assets/javascripts/app/controllers/layout_ref.coffee b/app/assets/javascripts/app/controllers/layout_ref.coffee index 1ae661cfb..f4dc268e7 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.coffee @@ -1499,7 +1499,7 @@ class InputsRef extends App.ControllerContent null: false relation: 'User' autocapitalize: false - disableCreateUser: true + disableCreateObject: true multiple: true @$('.userOrganizationAutocompletePlaceholder').replaceWith( userOrganizationAutocomplete.element() ) diff --git a/app/assets/javascripts/app/controllers/login.coffee b/app/assets/javascripts/app/controllers/login.coffee index 322a518c7..6e7f608d3 100644 --- a/app/assets/javascripts/app/controllers/login.coffee +++ b/app/assets/javascripts/app/controllers/login.coffee @@ -38,50 +38,7 @@ class Index extends App.ControllerContent ) render: (data = {}) -> - auth_provider_all = { - facebook: { - url: '/auth/facebook', - name: 'Facebook', - config: 'auth_facebook', - class: 'facebook' - }, - twitter: { - url: '/auth/twitter' - name: 'Twitter' - config: 'auth_twitter' - class: 'twitter' - }, - linkedin: { - url: '/auth/linkedin' - name: 'LinkedIn' - config: 'auth_linkedin' - class: 'linkedin' - }, - github: { - url: '/auth/github' - name: 'GitHub' - config: 'auth_github' - class: 'github' - }, - gitlab: { - url: '/auth/gitlab' - name: 'GitLab' - config: 'auth_gitlab' - class: 'gitlab' - }, - google_oauth2: { - url: '/auth/google_oauth2' - name: 'Google' - config: 'auth_google_oauth2' - class: 'google' - }, - oauth2: { - url: '/auth/oauth2' - name: 'OAuth2' - config: 'auth_oauth2' - class: 'oauth2' - }, - } + auth_provider_all = App.Config.get('auth_provider_all') auth_providers = [] for key, provider of auth_provider_all if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true' diff --git a/app/assets/javascripts/app/controllers/ticket_customer.coffee b/app/assets/javascripts/app/controllers/ticket_customer.coffee index e23ea954c..edb5df0c0 100644 --- a/app/assets/javascripts/app/controllers/ticket_customer.coffee +++ b/app/assets/javascripts/app/controllers/ticket_customer.coffee @@ -6,7 +6,7 @@ class App.TicketCustomer extends App.ControllerModal content: -> configure_attributes = [ - { name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateUser: true }, + { name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: true }, ] controller = new App.ControllerForm( model: diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index 66c5ec1ca..39f1c4e9a 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -221,7 +221,7 @@ class App.TicketOverview extends App.Controller if @batchCountIndex == @batchCount App.Event.trigger('overview:fetch') ) - return + return if action is 'group_assign' @batchCount = items.length diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index d523501f2..094a15f30 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -401,10 +401,11 @@ class App.TicketZoom extends App.Controller nav: @nav isCustomer: @permissionCheck('ticket.customer') scrollbarWidth: App.Utils.getScrollBarWidth() + dir: App.i18n.dir() ) new App.TicketZoomOverviewNavigator( - el: elLocal.find('.overview-navigator') + el: elLocal.find('.js-overviewNavigatorContainer') ticket_id: @ticket_id overview_id: @overview_id ) @@ -412,13 +413,13 @@ class App.TicketZoom extends App.Controller new App.TicketZoomTitle( object_id: @ticket_id overview_id: @overview_id - el: elLocal.find('.ticket-title') + el: elLocal.find('.js-ticketTitleContainer') task_key: @task_key ) new App.TicketZoomMeta( object_id: @ticket_id - el: elLocal.find('.ticket-meta') + el: elLocal.find('.js-ticketMetaContainer') ) @attributeBar = new App.TicketZoomAttributeBar( @@ -445,7 +446,12 @@ class App.TicketZoom extends App.Controller ) @highligher = new App.TicketZoomHighlighter( - el: elLocal.find('.highlighter') + el: elLocal.find('.js-highlighterContainer') + ticket_id: @ticket_id + ) + + new App.TicketZoomSetting( + el: elLocal.find('.js-settingContainer') ticket_id: @ticket_id ) @@ -467,6 +473,7 @@ class App.TicketZoom extends App.Controller sidebarState: @sidebarState object_id: @ticket_id model: 'Ticket' + query: @query taskGet: @taskGet task_key: @task_key formMeta: @formMeta @@ -557,14 +564,16 @@ class App.TicketZoom extends App.Controller return if !@ticket currentStoreTicket = @ticket.attributes() delete currentStoreTicket.article + internal = @Config.get('ui_ticket_zoom_article_note_new_internal') currentStore = ticket: currentStoreTicket article: to: '' cc: '' + subject: '' type: 'note' body: '' - internal: 'true' + internal: internal in_reply_to: '' if @permissionCheck('ticket.customer') @@ -575,7 +584,7 @@ class App.TicketZoom extends App.Controller formCurrent: => currentParams = ticket: @formParam(@el.find('.edit')) - article: @formParam(@el.find('.article-add')) + article: @articleNew.params() # add attachments if exist attachmentCount = @$('.article-add .textBubble .attachments .attachment').length @@ -684,7 +693,7 @@ class App.TicketZoom extends App.Controller tagAdd: (tag) => return if !@sidebar return if !@sidebar.reload - @sidebar.reload(tagAdd: tag) + @sidebar.reload(tagAdd: tag, source: 'macro') tagRemove: (tag) => return if !@sidebar return if !@sidebar.reload @@ -789,6 +798,9 @@ class App.TicketZoom extends App.Controller # reset form after save @reset() + if @sidebar + @sidebar.commit() + if taskAction is 'closeNextInOverview' if @overview_id current_position = 0 diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee index 6b60eb01e..cfa5a0ce4 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_actions.coffee @@ -391,6 +391,7 @@ class App.TicketZoomArticleActions extends App.Controller body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || '' # check if quote need to be added + signaturePosition = 'bottom' selected = App.ClipBoard.getSelected('html') if selected selected = App.Utils.htmlCleanup(selected).html() @@ -399,6 +400,16 @@ class App.TicketZoomArticleActions extends App.Controller if selected selected = App.Utils.textCleanup(selected) selected = App.Utils.text2html(selected) + + # full quote, if needed + if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote') + signaturePosition = 'top' + if article.content_type.match('html') + selected = App.Utils.textCleanup(article.body) + if article.content_type.match('plain') + selected = App.Utils.textCleanup(article.body) + selected = App.Utils.text2html(selected) + if selected selected = "
#{selected}
after enter on empty line sel = window.getSelection() - node = $(sel.anchorNode) - if (node.parent().is('blockquote')) { + if (sel) { + node = $(sel.anchorNode) + if (node && node.parent() && node.parent().is('blockquote')) { + e.preventDefault() + document.execCommand('Insertparagraph') + document.execCommand('Outdent') + return + } + } + + // behavior to enter new line on alt+enter + // on alt + enter not realy newline is fired, to make + // it compat. to other systems, do it here + if (!e.shiftKey && e.altKey && !e.ctrlKey && !e.metaKey) { e.preventDefault() - document.execCommand('Insertparagraph') - document.execCommand('Outdent') + _this.paste('
') return } } @@ -237,7 +248,7 @@ // limit check if ( !_this.allowKey(e) ) { - if ( !_this.maxLengthOk( 1 ) ) { + if ( !_this.maxLengthOk(1) ) { e.preventDefault() return } @@ -295,7 +306,7 @@ else { img = "" } - document.execCommand('insertHTML', false, img) + _this.paste(img) } // resize if to big @@ -367,13 +378,7 @@ text = App.Utils.removeEmptyLines(text) _this.log('insert', text) - // as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower) - if (docType == 'text3') { - _this.pasteHtmlAtCaret(text) - } - else { - document.execCommand('insertHTML', false, text) - } + _this.paste(text) return true }) @@ -533,37 +538,6 @@ return this.$element.html().trim() } - // taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294 - Plugin.prototype.pasteHtmlAtCaret = function(html) { - var sel, range; - if (window.getSelection) { - sel = window.getSelection() - if (sel.getRangeAt && sel.rangeCount) { - range = sel.getRangeAt(0) - range.deleteContents() - - var el = document.createElement('div') - el.innerHTML = html; - var frag = document.createDocumentFragment(), node, lastNode - while ( (node = el.firstChild) ) { - lastNode = frag.appendChild(node) - } - range.insertNode(frag) - - if (lastNode) { - range = range.cloneRange() - range.setStartAfter(lastNode) - range.collapse(true) - sel.removeAllRanges() - sel.addRange(range) - } - } - } - else if (document.selection && document.selection.type != 'Control') { - document.selection.createRange().pasteHTML(html) - } - } - // log method Plugin.prototype.log = function() { if (App && App.Log) { @@ -574,7 +548,30 @@ } } - $.fn[pluginName] = function ( options ) { + // paste some content + Plugin.prototype.paste = function(string) { + var isIE11 = !!window.MSInputMethodContext && !!document.documentMode; + + // IE <= 10 + if (document.selection && document.selection.createRange) { + var range = document.selection.createRange() + if (range.pasteHTML) { + range.pasteHTML(string) + } + } + // IE == 11 + else if (isIE11 && document.getSelection) { + var range = document.getSelection().getRangeAt(0) + var nnode = document.createElement('div') + range.surroundContents(nnode) + nnode.innerHTML = string + } + else { + document.execCommand('insertHTML', false, string) + } + } + + $.fn[pluginName] = function (options) { return this.each(function () { if (!$.data(this, 'plugin_' + pluginName)) { $.data(this, 'plugin_' + pluginName, @@ -586,6 +583,9 @@ // get correct val if textbox $.fn.ceg = function() { var plugin = $.data(this[0], 'plugin_' + pluginName) + if (!plugin) { + return + } return plugin.value() } diff --git a/app/assets/javascripts/app/lib/base/jquery.textmodule.js b/app/assets/javascripts/app/lib/base/jquery.textmodule.js index 492229a00..738d87249 100644 --- a/app/assets/javascripts/app/lib/base/jquery.textmodule.js +++ b/app/assets/javascripts/app/lib/base/jquery.textmodule.js @@ -43,17 +43,21 @@ this.$element.on('keydown', function (e) { - // esc - if (e.keyCode === 27) { - _this.close() - } - // navigate through item if (_this.isActive()) { + // esc + if (e.keyCode === 27) { + e.preventDefault() + e.stopPropagation() + _this.close() + return + } + // enter if (e.keyCode === 13) { e.preventDefault() + e.stopPropagation() var id = _this.$widget.find('.dropdown-menu li.is-active').data('id') // as fallback use hovered element @@ -72,12 +76,14 @@ // arrow keys left/right if (e.keyCode === 37 || e.keyCode === 39) { e.preventDefault() + e.stopPropagation() return } // up or down if (e.keyCode === 38 || e.keyCode === 40) { e.preventDefault() + e.stopPropagation() var active = _this.$widget.find('.dropdown-menu li.is-active') active.removeClass('is-active') @@ -92,6 +98,9 @@ var menu = _this.$widget.find('.dropdown-menu') + if (!active.get(0)) { + return + } if (active.position().top < 0) { // scroll up menu.scrollTop( menu.scrollTop() + active.position().top ) @@ -102,7 +111,11 @@ menu.scrollTop( menu.scrollTop() + invisibleHeight ) } } + } + // esc + if (e.keyCode === 27) { + _this.close() } }) @@ -180,14 +193,14 @@ Plugin.prototype.renderBase = function() { this.$element.after('') this.$widget = this.$element.next() - this.$widget.on('click', 'li', $.proxy(this.onEntryClick, this)) + this.$widget.on('mousedown', 'li', $.proxy(this.onEntryClick, this)) this.$widget.on('mouseenter', 'li', $.proxy(this.onMouseEnter, this)) } // set height of widget Plugin.prototype.movePosition = function() { if (!this._position) return - var height = this.$element.height() + 2 + var height = this.$element.outerHeight() + 2 var widgetHeight = this.$widget.find('ul').height() //+ 60 // + height var top = -( widgetHeight + height ) + this._position.top var left = this._position.left - 6 @@ -250,9 +263,21 @@ // paste some content Plugin.prototype.paste = function(string) { - if (document.selection) { // IE + var isIE11 = !!window.MSInputMethodContext && !!document.documentMode; + + // IE <= 10 + if (document.selection && document.selection.createRange) { var range = document.selection.createRange() - range.pasteHTML(string) + if (range.pasteHTML) { + range.pasteHTML(string) + } + } + // IE == 11 + else if (isIE11 && document.getSelection) { + var range = document.getSelection().getRangeAt(0) + var nnode = document.createElement('div') + range.surroundContents(nnode) + nnode.innerHTML = string } else { document.execCommand('insertHTML', false, string) @@ -295,14 +320,7 @@ // for chrome, insert space again if (start) { if (spacerChar === ' ') { - string = " " - if (document.selection) { // IE - var range = document.selection.createRange() - range.pasteHTML(string) - } - else { - document.execCommand('insertHTML', false, string) - } + this.paste(' ') } } } @@ -313,6 +331,7 @@ } Plugin.prototype.onEntryClick = function(event) { + event.preventDefault() var id = $(event.target).data('id') this.take(id) } @@ -325,7 +344,7 @@ } for (var i = 0; i < this.collection.length; i++) { var item = this.collection[i] - if ( item.id == id ) { + if (item.id == id) { var content = item.content this.cutInput() this.paste(content) diff --git a/app/assets/javascripts/app/lib/bootstrap/modal.js b/app/assets/javascripts/app/lib/bootstrap/modal.js index c9ddc93dc..4e202316c 100644 --- a/app/assets/javascripts/app/lib/bootstrap/modal.js +++ b/app/assets/javascripts/app/lib/bootstrap/modal.js @@ -10,6 +10,8 @@ modified by Felix Jan-2014 - add this.$body = $(options.container || document.body) - adjustBackdrop: also adopt left, top and width from $body + modified by Felix Jul-2017 + - add rtl support */ @@ -244,6 +246,10 @@ .css('height', 0) .css('height', this.$element[0].scrollHeight) + if(App.i18n.dir() == 'rtl'){ + this.$backdrop.css('right', 'auto') + } + if(this.scrollbarWidth){ this.$backdrop.css('width', this.$body.width() - this.scrollbarWidth) } @@ -251,14 +257,22 @@ Modal.prototype.adjustDialog = function () { var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight - - this.$element.css({ + var css = { left: this.$body.offset().left, top: this.$body.offset().top, width: this.$body.width(), paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' - }) + } + + if(App.i18n.dir() == 'rtl'){ + css.right = 'auto' + var paddingLeft = css.paddingLeft + css.paddingLeft = css.paddingRight + css.paddingRight = paddingLeft + } + + this.$element.css(css) } Modal.prototype.resetAdjustments = function () { diff --git a/app/assets/javascripts/app/models/_application_model.coffee b/app/assets/javascripts/app/models/_application_model.coffee index f7c708b78..49ad8aac9 100644 --- a/app/assets/javascripts/app/models/_application_model.coffee +++ b/app/assets/javascripts/app/models/_application_model.coffee @@ -32,6 +32,10 @@ class App.Model extends Spine.Model return @title if @subject return @subject + if @phone + return @phone + if @login + return @login return '???' displayNameLong: -> @@ -57,6 +61,12 @@ class App.Model extends Spine.Model return @email if @title return @title + if @subject + return @subject + if @phone + return @phone + if @login + return @login return '???' icon: (user) -> @@ -165,6 +175,31 @@ class App.Model extends Spine.Model ### +set new attributes of model (remove already available attributes) + + App.Model.attributesSet(attributes) + + ### + + @attributesSet: (attributes) -> + + configure_attributes = App[ @.className ].configure_attributes + attributesNew = [] + for localAttribute in configure_attributes + found = false + for attribute in attributes + if attribute.name is localAttribute.name + found = true + break + if !found + attributesNew.push localAttribute + for attribute in attributes + App[@.className].attributes.push attribute.name + attributesNew.push attribute + App[ @.className ].configure_attributes = attributesNew + + ### + attributes = App.Model.attributesGet(optionalScreen, optionalAttributesList) returns diff --git a/app/assets/javascripts/app/models/job.coffee b/app/assets/javascripts/app/models/job.coffee index 996e3dfc7..d061c2db0 100644 --- a/app/assets/javascripts/app/models/job.coffee +++ b/app/assets/javascripts/app/models/job.coffee @@ -6,7 +6,7 @@ class App.Job extends App.Model { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, { name: 'timeplan', display: 'When should the job run?', tag: 'timer', null: true }, { name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: true }, - { name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true }, + { name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true, ticket_delete: true }, { name: 'disable_notification', display: 'Disable Notifications', tag: 'boolean', default: true }, { name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true }, { name: 'active', display: 'Active', tag: 'active', default: true }, diff --git a/app/assets/javascripts/app/models/overview.coffee b/app/assets/javascripts/app/models/overview.coffee index 144f228f0..dd8c0b2e9 100644 --- a/app/assets/javascripts/app/models/overview.coffee +++ b/app/assets/javascripts/app/models/overview.coffee @@ -8,6 +8,7 @@ class App.Overview extends App.Model { name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, null: false, relation: 'Role', translate: true }, { name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, null: true, relation: 'User', sortBy: 'firstname' }, { name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true }, + { name: 'out_of_office', display: 'Only available for Users which are replacements for other users.', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true }, { name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false }, { name: 'prio', display: 'Prio', readonly: 1 }, { @@ -72,4 +73,4 @@ Sie können auch individuelle Übersichten für einzelne Agenten oder agenten Gr ''' uiUrl: -> - '#ticket/view/' + @link + "#ticket/view/#{@link}" diff --git a/app/assets/javascripts/app/models/postmaster_filter.coffee b/app/assets/javascripts/app/models/postmaster_filter.coffee index 30c2fa5b3..2ebddcc10 100644 --- a/app/assets/javascripts/app/models/postmaster_filter.coffee +++ b/app/assets/javascripts/app/models/postmaster_filter.coffee @@ -6,7 +6,7 @@ class App.PostmasterFilter extends App.Model @configure_attributes = [ { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 250, 'null': false }, { name: 'channel', display: 'Channel', type: 'input', readonly: 1 }, - { name: 'match', display: 'Match all of the following', tag: 'postmaster_match' }, + { name: 'match', display: 'Match all of the following', tag: 'postmaster_match', note: 'You can use regular expression by using "regex:your_reg_exp".' }, { name: 'perform', display: 'Perform action of the following', tag: 'postmaster_set' }, { name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, diff --git a/app/assets/javascripts/app/models/user.coffee b/app/assets/javascripts/app/models/user.coffee index ae3641619..7d357369c 100644 --- a/app/assets/javascripts/app/models/user.coffee +++ b/app/assets/javascripts/app/models/user.coffee @@ -53,8 +53,14 @@ class App.User extends App.Model cssClass += ' ' if cssClass cssClass += "size-#{ size }" + if @active is false + cssClass += ' avatar--inactive' + + if @isOutOfOffice() + cssClass += ' avatar--vacation' + if placement - placement = " data-placement='#{ placement }'" + placement = " data-placement='#{placement}'" if !avatar if type is 'personal' @@ -104,6 +110,19 @@ class App.User extends App.Model vip: vip url: @imageUrl() + isOutOfOffice: -> + return false if @out_of_office isnt true + start_time = @out_of_office_start_at + return false if !start_time + end_time = @out_of_office_end_at + return false if !end_time + start_time = new Date(Date.parse(start_time)) + end_time = new Date(Date.parse(end_time)) + now = new Date((new Date).toDateString()) + if start_time <= now && end_time >= now + return true + false + imageUrl: -> return if !@image # set image url @@ -237,3 +256,16 @@ class App.User extends App.Model break return access if access false + + @outOfOfficeTextPlaceholder: -> + today = new Date() + outOfOfficeText = 'Christmas holiday' + if today.getMonth() < 3 + outOfOfficeText = 'Easter holiday' + else if today.getMonth() < 9 + outOfOfficeText = 'Summer holiday' + outOfOfficeText + + outOfOfficeText: -> + return @preferences.out_of_office_text if !_.isEmpty(@preferences.out_of_office_text) + App.User.outOfOfficeTextPlaceholder() diff --git a/app/assets/javascripts/app/views/channel/chat.jst.eco b/app/assets/javascripts/app/views/channel/chat.jst.eco index cfdd447af..310183074 100644 --- a/app/assets/javascripts/app/views/channel/chat.jst.eco +++ b/app/assets/javascripts/app/views/channel/chat.jst.eco @@ -142,7 +142,7 @@<%- @T('Automatically show chat') %> (<%- @T('default') %>)
<%- @T('The chat will show up once the connection to the server got established and if there is someone online to chat with.') %>
-<script src="<%= @baseurl %>/assets/chat/chat.min.js"></script> +
<script src="<%= @baseurl %>/assets/chat/chat.min.js"></script> <script> $(function() { new ZammadChat({ @@ -153,7 +153,7 @@ $(function() {
<%- @T('Manually open chat') %>
<%- @T('If you want to open the chat by the press of a button set the option §show§ to §false§ and add the class §open-zammad-chat§ to the button.') %>
-<button class="open-zammad-chat">Chat with us</button> +
<button class="open-zammad-chat">Chat with us</button> <script src="<%= @baseurl %>/assets/chat/chat.min.js"></script> <script> diff --git a/app/assets/javascripts/app/views/channel/form.jst.eco b/app/assets/javascripts/app/views/channel/form.jst.eco index f0e412a8f..011c10a8c 100644 --- a/app/assets/javascripts/app/views/channel/form.jst.eco +++ b/app/assets/javascripts/app/views/channel/form.jst.eco @@ -126,6 +126,9 @@ +
<%- @T('Requirements') %>
+<%- @T("Zammad Forms requires jQuery. If you don't already use it on your website include it like this:") %>
+<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<%- @T('You need to add the following Javascript code snippet to your web page') %>:
diff --git a/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco b/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco index 8e6710ef7..1fef2ed9b 100644 --- a/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco +++ b/app/assets/javascripts/app/views/customer_chat/chat_window.jst.eco @@ -26,6 +26,8 @@- +\ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/checkbox.jst.eco b/app/assets/javascripts/app/views/generic/checkbox.jst.eco index 4576fab6f..8d0412aad 100644 --- a/app/assets/javascripts/app/views/generic/checkbox.jst.eco +++ b/app/assets/javascripts/app/views/generic/checkbox.jst.eco @@ -4,7 +4,7 @@ /> <%- @Icon('checkbox', 'icon-unchecked') %> <%- @Icon('checkbox-checked', 'icon-checked') %> - <%= row.name %> <% if row.note: %>- <%= row.note %><% end %> + <%= row.name %> <% if row.note: %>- <%- @T(row.note) %><% end %> <% end %> \ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/radio.jst.eco b/app/assets/javascripts/app/views/generic/radio.jst.eco index ea7282ac2..d6576004a 100644 --- a/app/assets/javascripts/app/views/generic/radio.jst.eco +++ b/app/assets/javascripts/app/views/generic/radio.jst.eco @@ -1,7 +1,7 @@+ +<%- @T('Send') %><% for row in @attribute.options: %> diff --git a/app/assets/javascripts/app/views/generic/sidebar_tabs.jst.eco b/app/assets/javascripts/app/views/generic/sidebar_tabs.jst.eco index 30a3ada73..d1bd32527 100644 --- a/app/assets/javascripts/app/views/generic/sidebar_tabs.jst.eco +++ b/app/assets/javascripts/app/views/generic/sidebar_tabs.jst.eco @@ -11,7 +11,7 @@<% end %> -