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}

" @@ -409,7 +420,12 @@ class App.TicketZoomArticleActions extends App.Controller type = App.TicketArticleType.findByAttribute(name:'email') - App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } ) + App.Event.trigger('ui::ticket::setArticleType', { + ticket: @ticket + type: type + article: articleNew + signaturePosition: signaturePosition + }) telegramPersonalMessageReply: (e) => e.preventDefault() diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index c42c5ba7a..edb39480d 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -28,7 +28,62 @@ class App.TicketZoomArticleNew extends App.Controller constructor: -> super - # set possble article types + @internalSelector = true + @type = @defaults['type'] || 'note' + @setPossibleArticleTypes() + + if @permissionCheck('ticket.customer') + @internalSelector = false + + @textareaHeight = + open: 148 + closed: 20 + + @dragEventCounter = 0 + @attachments = [] + + @render() + + if @defaults.body or @isIE10() + @openTextarea(null, true) + + # set article type and expand text area + @bind('ui::ticket::setArticleType', (data) => + return if data.ticket.id.toString() isnt @ticket_id.toString() + + @openTextarea(null, true) + for key, value of data.article + if key is 'body' + @$('[data-name="' + key + '"]').html(value) + else + @$('[name="' + key + '"]').val(value).trigger('change') + + # preselect article type + @setArticleType(data.type.name, data.signaturePosition) + + # set focus at end of field + if data.position is 'end' + @placeCaretAtEnd(@textarea.get(0)) + return + + # set focus into field + @textarea.focus() + ) + + # reset new article screen + @bind('ui::ticket::taskReset', (data) => + return if data.ticket_id.toString() isnt @ticket_id.toString() + @type = 'note' + @defaults = {} + @render() + ) + + # rerender, e. g. on language change + @bind('ui:rerender', => + @render() + ) + + setPossibleArticleTypes: => possibleArticleType = note: true phone: true @@ -50,12 +105,9 @@ class App.TicketZoomArticleNew extends App.Controller possibleArticleType['email'] = true # gets referenced in @setArticleType - @internalSelector = true - @type = @defaults['type'] || 'note' @articleTypes = [] if possibleArticleType.note - internal = @Config.get('ui_ticket_zoom_article_new_internal') - + internal = @Config.get('ui_ticket_zoom_article_note_new_internal') @articleTypes.push { name: 'note' icon: 'note' @@ -64,10 +116,13 @@ class App.TicketZoomArticleNew extends App.Controller features: ['attachment'] } if possibleArticleType.email + attributes = ['to', 'cc', 'subject'] + if !@Config.get('ui_ticket_zoom_article_email_subject') + attributes = ['to', 'cc'] @articleTypes.push { name: 'email' icon: 'email' - attributes: ['to', 'cc'] + attributes: attributes internal: false, features: ['attachment'] } @@ -80,6 +135,9 @@ class App.TicketZoomArticleNew extends App.Controller features: [] } if possibleArticleType['twitter status'] + attributes = ['body:limit', 'body:initials'] + if !@Config.get('ui_ticket_zoom_article_twitter_initials') + attributes = ['body:limit'] @articleTypes.push { name: 'twitter status' icon: 'twitter' @@ -90,6 +148,9 @@ class App.TicketZoomArticleNew extends App.Controller warningTextLength: 30 } if possibleArticleType['twitter direct-message'] + attributes = ['body:limit', 'body:initials'] + if !@Config.get('ui_ticket_zoom_article_twitter_initials') + attributes = ['body:limit'] @articleTypes.push { name: 'twitter direct-message' icon: 'twitter' @@ -130,57 +191,6 @@ class App.TicketZoomArticleNew extends App.Controller }, ] - if @permissionCheck('ticket.customer') - @internalSelector = false - - @textareaHeight = - open: 148 - closed: 20 - - @dragEventCounter = 0 - @attachments = [] - - @render() - - if @defaults.body or @isIE10() - @openTextarea(null, true) - - # set article type and expand text area - @bind('ui::ticket::setArticleType', (data) => - return if data.ticket.id.toString() isnt @ticket_id.toString() - - @openTextarea(null, true) - for key, value of data.article - if key is 'body' - @$('[data-name="' + key + '"]').html(value) - else - @$('[name="' + key + '"]').val(value).trigger('change') - - # preselect article type - @setArticleType(data.type.name) - - # set focus at end of field - if data.position is 'end' - @placeCaretAtEnd(@textarea.get(0)) - return - - # set focus into field - @textarea.focus() - ) - - # reset new article screen - @bind('ui::ticket::taskReset', (data) => - return if data.ticket_id.toString() isnt @ticket_id.toString() - @type = 'note' - @defaults = {} - @render() - ) - - # rerender, e. g. on language change - @bind('ui:rerender', => - @render() - ) - placeCaretAtEnd: (el) -> el.focus() if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined' @@ -300,6 +310,7 @@ class App.TicketZoomArticleNew extends App.Controller data: ticket: ticket user: App.Session.get() + config: App.Config.all() ) callback = (ticket) -> textModule.reload( @@ -318,9 +329,6 @@ class App.TicketZoomArticleNew extends App.Controller params.form_id = @form_id params.content_type = 'text/html' - if !params['internal'] - params['internal'] = false - if @permissionCheck('ticket.customer') sender = App.TicketArticleSender.findByAttribute('name', 'Customer') type = App.TicketArticleType.findByAttribute('name', 'web') @@ -332,6 +340,11 @@ class App.TicketZoomArticleNew extends App.Controller params.sender_id = sender.id params.type_id = type.id + if params.internal + params.internal = true + else + params.internal = false + if params.type is 'twitter status' App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false) params.content_type = 'text/plain' @@ -471,13 +484,15 @@ class App.TicketZoomArticleNew extends App.Controller @$('[name=internal]').val('') - setArticleType: (type) => + setArticleType: (type, signaturePosition = 'bottom') => wasScrolledToBottom = @isScrolledToBottom() @type = type @$('[name=type]').val(type).trigger('change') @articleNewEdit.attr('data-type', type) @$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide') + @setPossibleArticleTypes() + # get config config = {} for articleTypeConfig in @articleTypes @@ -510,7 +525,7 @@ class App.TicketZoomArticleNew extends App.Controller @$('[data-name=body] [data-signature="true"]').remove() # apply new signature - signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), ticket: ticketCurrent }) + signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), ticket: ticketCurrent, config: App.Config.all() }) body = @$('[data-name=body]') if App.Utils.signatureCheck(body.html() || '', signatureFinished) @@ -518,7 +533,10 @@ class App.TicketZoomArticleNew extends App.Controller body.append('

') signature = $("
#{signatureFinished}
") App.Utils.htmlStrip(signature) - body.append(signature) + if signaturePosition is 'top' + body.prepend(signature) + else + body.append(signature) @$('[data-name=body]').replaceWith(body) # remove old signature @@ -552,6 +570,20 @@ class App.TicketZoomArticleNew extends App.Controller @delay(@updateLetterCount, 600) @$('.js-textSizeLimit').removeClass('hide') + # convert remote src images to data uri + @$('[data-name=body] img').each( (i,image) -> + $image = $(image) + src = $image.attr('src') + if !_.isEmpty(src) && !src.match(/^data:image/i) + canvas = document.createElement('canvas') + canvas.width = image.width + canvas.height = image.height + ctx = canvas.getContext('2d') + ctx.drawImage(image, 0, 0) + dataURL = canvas.toDataURL() + $image.attr('src', dataURL) + ) + @scrollToBottom() if wasScrolledToBottom isScrolledToBottom: -> diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/higlighter.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/highlighter.coffee similarity index 100% rename from app/assets/javascripts/app/controllers/ticket_zoom/higlighter.coffee rename to app/assets/javascripts/app/controllers/ticket_zoom/highlighter.coffee diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/setting.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/setting.coffee new file mode 100644 index 000000000..1103f9e3c --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/setting.coffee @@ -0,0 +1,35 @@ +class App.TicketZoomSetting extends App.Controller + events: + 'click .js-setting': 'show' + + constructor: -> + super + return if !@permissionCheck('admin') + @render() + + render: -> + @html(App.view('ticket_zoom/setting')()) + + show: -> + new Modal() + +class Modal extends App.ControllerModal + buttonClose: true + buttonCancel: true + buttonSubmit: false + head: 'Settings' + + constructor: -> + super + + render: => + super + + post: => + new App.SettingsArea( + area: 'UI::TicketZoom' + el: @el.find('.modal-body') + ) + + content: -> + App.view('generic/page_loading')() diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee index 874922998..1eb4d68ea 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee @@ -9,18 +9,32 @@ class App.TicketZoomSidebar extends App.ObserverController if backend && backend.reload backend.reload(args) + commit: (args) => + for key, backend of @sidebarBackends + if backend && backend.commit + backend.commit(args) + render: (ticket) => - @sidebarBackends = {} + @sidebarBackends ||= {} @sidebarItems = [] sidebarBackends = App.Config.get('TicketZoomSidebar') keys = _.keys(sidebarBackends).sort() for key in keys - @sidebarBackends[key] = new sidebarBackends[key]( - ticket: ticket - taskGet: @taskGet - formMeta: @formMeta - markForm: @markForm - ) + if !@sidebarBackends[key] || !@sidebarBackends[key].reload + @sidebarBackends[key] = new sidebarBackends[key]( + ticket: ticket + query: @query + taskGet: @taskGet + formMeta: @formMeta + markForm: @markForm + ) + else + @sidebarBackends[key].reload( + params: @params + query: @query + formMeta: @formMeta + markForm: @markForm + ) item = @sidebarBackends[key].sidebarItem() if item @sidebarItems.push item diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_customer.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_customer.coffee index 701a151b6..92bfd8c65 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_customer.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_customer.coffee @@ -1,7 +1,7 @@ class SidebarCustomer extends App.Controller sidebarItem: => return if !@permissionCheck('ticket.agent') - { + items = { head: 'Customer' name: 'customer' icon: 'person' @@ -11,14 +11,16 @@ class SidebarCustomer extends App.Controller name: 'customer-change' callback: @changeCustomer }, - { - title: 'Edit Customer' - name: 'customer-edit' - callback: @editCustomer - }, ] callback: @showCustomer } + return items if @ticket && @ticket.customer_id == 1 + items.actions.push { + title: 'Edit Customer' + name: 'customer-edit' + callback: @editCustomer + } + items showCustomer: (el) => @el = el diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_idoit.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_idoit.coffee new file mode 100644 index 000000000..5ebce7924 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_idoit.coffee @@ -0,0 +1,132 @@ +class SidebarIdoit extends App.Controller + sidebarItem: => + return if !@Config.get('idoit_integration') + { + head: 'i-doit' + name: 'idoit' + icon: 'printer' + actions: [ + { + title: 'Change Objects' + name: 'objects-change' + callback: @changeObjects + }, + ] + callback: @showObjects + } + + changeObjects: => + new App.IdoitObjectSelector( + task_key: @task_key + container: @el.closest('.content') + callback: (objectIds, objectSelectorUi) => + if @ticket && @ticket.id + @updateTicket(@ticket.id, objectIds, => + objectSelectorUi.close() + @showObjectsContent(objectIds) + ) + return + objectSelectorUi.close() + @showObjectsContent(objectIds) + ) + + showObjects: (el) => + @el = el + + # show placeholder + @objectIds ||= [] + if @ticket && @ticket.preferences && @ticket.preferences.idoit && @ticket.preferences.idoit.object_ids + @objectIds = @ticket.preferences.idoit.object_ids + queryParams = @queryParam() + if queryParams && queryParams.idoit_object_ids + @objectIds.push queryParams.idoit_object_ids + @showObjectsContent() + + showObjectsContent: (objectIds) => + if objectIds + @objectIds = @objectIds.concat(objectIds) + + # show placeholder + if _.isEmpty(@objectIds) + @html("
#{App.i18n.translateInline('none')}
") + return + + # ajax call to show items + @ajax( + id: "idoit-#{@task_key}" + type: 'POST' + url: "#{@apiPath}/integration/idoit" + data: JSON.stringify(method: 'cmdb.objects', filter: ids: @objectIds) + success: (data, status, xhr) => + if data.response + @showList(data.response.result) + return + @showError('Unable to load data...') + + error: (xhr, status, error) => + + # do not close window if request is aborted + return if status is 'abort' + + # show error message + @showError('Unable to load data...') + ) + + showList: (objects) => + list = $(App.view('ticket_zoom/sidebar_idoit')( + objects: objects + )) + list.delegate('.js-delete', 'click', (e) => + e.preventDefault() + objectId = $(e.currentTarget).attr 'data-object-id' + @delete(objectId) + ) + @html(list) + + showError: (message) => + @html App.i18n.translateInline(message) + + delete: (objectId) => + localObjects = [] + for localObjectId in @objectIds + if objectId.toString() isnt localObjectId.toString() + localObjects.push localObjectId + @objectIds = localObjects + if @ticket && @ticket.id + @updateTicket(@ticket.id, @objectIds) + @showObjectsContent() + + commit: (args) => + return if @ticket && @ticket.id + return if !@objectIds + return if _.isEmpty(@objectIds) + return if !args + return if !args.ticket_id + @updateTicket(args.ticket_id, @objectIds) + + updateTicket: (ticket_id, objectIds, callback) => + App.Ajax.request( + id: "idoit-update-#{ticket_id}" + type: 'POST' + url: "#{@apiPath}/integration/idoit_ticket_update" + data: JSON.stringify(ticket_id: ticket_id, object_ids: objectIds) + success: (data, status, xhr) -> + if callback + callback(objectIds) + + error: (xhr, status, details) => + + # do not close window if request is aborted + return if status is 'abort' + + # show error message + @log 'errors', details + @notify( + type: 'error' + msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to update object!') + timeout: 6000 + ) + ) + +App.Config.set('500-Idoit', SidebarIdoit, 'TicketCreateSidebar') +App.Config.set('500-Idoit', SidebarIdoit, 'TicketZoomSidebar') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_organization.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_organization.coffee index ad612da6b..6fc2ef5cb 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_organization.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_organization.coffee @@ -1,5 +1,6 @@ class SidebarOrganization extends App.Controller sidebarItem: => + return if !@permissionCheck('ticket.agent') return if !@ticket.organization_id { head: 'Organization' diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index 52ea11299..cb656f976 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -69,7 +69,7 @@ class SidebarTicket extends App.Controller if args.tags @tagWidget.reload(args.tags) if args.tagAdd - @tagWidget.add(args.tagAdd) + @tagWidget.add(args.tagAdd, args.source) if args.tagRemove @tagWidget.remove(args.tagRemove) diff --git a/app/assets/javascripts/app/controllers/widget/avatar.coffee b/app/assets/javascripts/app/controllers/widget/avatar.coffee index c2ce3e055..e6bc84d35 100644 --- a/app/assets/javascripts/app/controllers/widget/avatar.coffee +++ b/app/assets/javascripts/app/controllers/widget/avatar.coffee @@ -7,6 +7,12 @@ class App.WidgetAvatar extends App.ObserverController email: true image: true vip: true + out_of_office: true, + out_of_office_start_at: true, + out_of_office_end_at: true, + out_of_office_replacement_id: true, + active: true + globalRerender: false render: (user) => diff --git a/app/assets/javascripts/app/controllers/widget/dev_banner.coffee b/app/assets/javascripts/app/controllers/widget/dev_banner.coffee index a8f7f5c9e..28d534391 100644 --- a/app/assets/javascripts/app/controllers/widget/dev_banner.coffee +++ b/app/assets/javascripts/app/controllers/widget/dev_banner.coffee @@ -5,10 +5,10 @@ class Widget banner = """ | | Welcome Zammad Developer! -| You can enable debugging by the following examples (value is a regex): +| You can enable debugging with the following examples (value is a regex): | | App.Log.config('module', '(websocket|delay|interval)') // enable debugging for websocket, delay and interval class -| App.Log.config('content', 'send') // enable debugging for messages which contains the string 'send' +| App.Log.config('content', 'send') // enable debugging for messages which contain the string 'send' | App.Log.config('banner', false) // disable this banner | | App.Log.config() // current settings diff --git a/app/assets/javascripts/app/controllers/widget/hello_banner.coffee b/app/assets/javascripts/app/controllers/widget/hello_banner.coffee index 966d81055..9c0d9e8fa 100644 --- a/app/assets/javascripts/app/controllers/widget/hello_banner.coffee +++ b/app/assets/javascripts/app/controllers/widget/hello_banner.coffee @@ -10,9 +10,9 @@ class Widget | | Hi there, nice to meet you! | -| Visit %chttps://zammad.org/participate%c and let's make Zammad better. +| Visit %chttp://zammad.com/jobs%c to learn about our current job openings. | -| The Zammad Team. +| Your Zammad Team | """ console.log(banner, 'text-decoration: underline;', 'text-decoration: none;') diff --git a/app/assets/javascripts/app/controllers/widget/http_log.coffee b/app/assets/javascripts/app/controllers/widget/http_log.coffee index 4a625b3bd..43ab07baf 100644 --- a/app/assets/javascripts/app/controllers/widget/http_log.coffee +++ b/app/assets/javascripts/app/controllers/widget/http_log.coffee @@ -25,6 +25,7 @@ class App.HttpLog extends App.Controller render: => @html App.view('widget/http_log')( records: @records + description: @description ) show: (e) => diff --git a/app/assets/javascripts/app/controllers/widget/script_snipped.coffee b/app/assets/javascripts/app/controllers/widget/script_snipped.coffee new file mode 100644 index 000000000..ec2fb1d6e --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/script_snipped.coffee @@ -0,0 +1,24 @@ +class App.ScriptSnipped extends App.Controller + #events: + # 'click .js-record': 'show' + + elements: + '.js-code': 'code' + + + constructor: -> + super + #@fetch() + @records = [] + @render() + + render: => + @html App.view('widget/script_snipped')( + records: @records + description: @description + style: @style + content: @content + ) + + @code.each (i, block) -> + hljs.highlightBlock block \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/widget/tag.coffee b/app/assets/javascripts/app/controllers/widget/tag.coffee index f148746fa..ee0f4b455 100644 --- a/app/assets/javascripts/app/controllers/widget/tag.coffee +++ b/app/assets/javascripts/app/controllers/widget/tag.coffee @@ -86,16 +86,16 @@ class App.WidgetTag extends App.Controller return @add(item) - add: (items) => + add: (items, source = '') => for item in items.split(',') item = item.trim() - @addItem(item) + @addItem(item, source) - addItem: (item) => + addItem: (item, source = '') => if _.contains(@localTags, item) @render() return - return if App.Config.get('tag_new') is false && !@possibleTags[item] + return if source != 'macro' && App.Config.get('tag_new') is false && !@possibleTags[item] @localTags.push item @render() App[@object_type].tagAdd(@object.id, item) diff --git a/app/assets/javascripts/app/controllers/widget/text_module.coffee b/app/assets/javascripts/app/controllers/widget/text_module.coffee index b4027bfeb..09dc5468c 100644 --- a/app/assets/javascripts/app/controllers/widget/text_module.coffee +++ b/app/assets/javascripts/app/controllers/widget/text_module.coffee @@ -8,7 +8,7 @@ class App.WidgetTextModule extends App.Controller # remember instances @bindElements = [] if @selector - @bindElements = @$( @selector ).textmodule() + @bindElements = @$(@selector).textmodule() else if @el.attr('contenteditable') @bindElements = @el.textmodule() diff --git a/app/assets/javascripts/app/lib/app_init/queue_manager.coffee b/app/assets/javascripts/app/lib/app_init/queue_manager.coffee new file mode 100644 index 000000000..e560550f0 --- /dev/null +++ b/app/assets/javascripts/app/lib/app_init/queue_manager.coffee @@ -0,0 +1,57 @@ +class App.QueueManager + _instance = undefined + + @init: -> + _instance ?= new _queueSingleton + + @add: (key, data) -> + if _instance == undefined + _instance ?= new _queueSingleton + _instance.add(key, data) + + @pull: (key) -> + if _instance == undefined + _instance ?= new _queueSingleton + _instance.pull(key) + + @all: (key) -> + if _instance == undefined + _instance ?= new _queueSingleton + _instance.all(key) + + @run: (key, callback) -> + if _instance == undefined + _instance ?= new _queueSingleton + _instance.run(key, callback) + +class _queueSingleton + constructor: -> + @queues = {} + @queueRunning = {} + + add: (key, data) -> + if !@queues[key] + @queues[key] = [] + @queues[key].push data + true + + pull: (key) -> + return if !@queues[key] + @queues[key].shift() + + all: (key) -> + @queues[key] + + run: (key, callback) -> + return if !@queues[key] + return if @queueRunning[key] + localQueue = @queues[key] + return if _.isEmpty(localQueue) + @queueRunning[key] = true + loop + callback = localQueue.shift() + callback() + if !localQueue[0] + @queueRunning[key] = false + break + true diff --git a/app/assets/javascripts/app/lib/app_post/_collection_base.coffee b/app/assets/javascripts/app/lib/app_post/_collection_base.coffee index 4fa13b67d..d031dfa11 100644 --- a/app/assets/javascripts/app/lib/app_post/_collection_base.coffee +++ b/app/assets/javascripts/app/lib/app_post/_collection_base.coffee @@ -5,9 +5,9 @@ class App._CollectionSingletonBase constructor: -> @callbacks = {} @counter = 0 - + @key = "collection-#{@event}" # read from cache - cache = App.SessionStorage.get("collection-#{@event}") + cache = App.SessionStorage.get(@key) if cache @set(cache) @@ -73,6 +73,9 @@ class App._CollectionSingletonBase callback: (data) => for counter, attr of @callbacks - attr.callback(data) - if attr.one - delete @callbacks[counter] + callback = -> + attr.callback(data) + if attr.one + delete @callbacks[counter] + App.QueueManager.add(@key, callback) + App.QueueManager.run(@key) diff --git a/app/assets/javascripts/app/lib/app_post/column_select.coffee b/app/assets/javascripts/app/lib/app_post/column_select.coffee index d58354f5d..728c2d499 100644 --- a/app/assets/javascripts/app/lib/app_post/column_select.coffee +++ b/app/assets/javascripts/app/lib/app_post/column_select.coffee @@ -33,6 +33,11 @@ class App.ColumnSelect extends Spine.Controller @select @pickedValue , 300, {trailing: false} + if @attribute.onChange + @shadow.on('change', => + @attribute.onChange(@shadow.val()) + ) + render: -> @values = [] _.each @options.attribute.options, (option) => diff --git a/app/assets/javascripts/app/lib/app_post/delay.coffee b/app/assets/javascripts/app/lib/app_post/delay.coffee index c68fdd3b9..284848852 100644 --- a/app/assets/javascripts/app/lib/app_post/delay.coffee +++ b/app/assets/javascripts/app/lib/app_post/delay.coffee @@ -1,10 +1,10 @@ class App.Delay _instance = undefined - @set: (callback, timeout, key, level) -> + @set: (callback, timeout, key, level, queue) -> if _instance == undefined _instance ?= new _delaySingleton - _instance.set(callback, timeout, key, level) + _instance.set(callback, timeout, key, level, queue) @clear: (key, level) -> if _instance == undefined @@ -21,6 +21,11 @@ class App.Delay _instance ?= new _delaySingleton _instance.reset() + @count: -> + if _instance == undefined + _instance ?= new _intervalSingleton + _instance.count() + @_all: -> if _instance == undefined _instance ?= new _delaySingleton @@ -32,7 +37,7 @@ class _delaySingleton extends Spine.Module constructor: -> @levelStack = {} - set: (callback, timeout, key, level) => + set: (callback, timeout, key, level, queue) => if !level level = '_all' @@ -44,11 +49,15 @@ class _delaySingleton extends Spine.Module key = Math.floor(Math.random() * 99999) # setTimeout - @log 'debug', 'set', key, timeout, level, callback - call = => + @log 'debug', 'set', key, timeout, level, callback, queue + localCallback = => @clear(key, level) - callback() - delay_id = setTimeout(call, timeout) + if queue + App.QueueManager.add('delay', callback) + App.QueueManager.run('delay') + else + callback() + delay_id = setTimeout(localCallback, timeout) # remember all delays if !@levelStack[level] @@ -93,6 +102,13 @@ class _delaySingleton extends Spine.Module @levelStack[level] = {} true + count: => + return 0 if !@levelStack + count = 0 + for levelName, levelValue of @levelStack + count += Object.keys(levelValue).length + count + _all: => @levelStack diff --git a/app/assets/javascripts/app/lib/app_post/i18n.coffee b/app/assets/javascripts/app/lib/app_post/i18n.coffee index 3dd198b70..560562269 100644 --- a/app/assets/javascripts/app/lib/app_post/i18n.coffee +++ b/app/assets/javascripts/app/lib/app_post/i18n.coffee @@ -30,6 +30,11 @@ class App.i18n _instance ?= new _i18nSingleton() _instance.date(args, offset) + @dir: -> + if _instance == undefined + _instance ?= new _i18nSingleton() + _instance.dir() + @get: -> if _instance == undefined _instance ?= new _i18nSingleton() @@ -88,6 +93,10 @@ class _i18nSingleton extends Spine.Module @_notTranslated = {} @dateFormat = 'yyyy-mm-dd' @timestampFormat = 'yyyy-mm-dd HH:MM' + @dirToSet = 'ltr' + + dir: -> + @dirToSet get: -> @locale @@ -96,7 +105,7 @@ class _i18nSingleton extends Spine.Module # prepare locale localeToSet = localeToSet.toLowerCase() - dirToSet = 'ltr' + @dirToSet = 'ltr' # check if locale exists localeFound = false @@ -104,7 +113,7 @@ class _i18nSingleton extends Spine.Module for locale in locales if locale.locale is localeToSet localeToSet = locale.locale - dirToSet = locale.dir + @dirToSet = locale.dir localeFound = true # try aliases @@ -112,7 +121,7 @@ class _i18nSingleton extends Spine.Module for locale in locales if locale.alias is localeToSet localeToSet = locale.locale - dirToSet = locale.dir + @dirToSet = locale.dir localeFound = true # if no locale and no alias was found, try to find correct one @@ -123,7 +132,7 @@ class _i18nSingleton extends Spine.Module for locale in locales if locale.alias is localeToSet localeToSet = locale.locale - dirToSet = locale.dir + @dirToSet = locale.dir localeFound = true # check if locale need to be changed @@ -137,7 +146,7 @@ class _i18nSingleton extends Spine.Module # set lang and dir attribute of html tag $('html').prop('lang', localeToSet.substr(0, 2)) - $('html').prop('dir', dirToSet) + $('html').prop('dir', @dirToSet) @mapString = {} App.Ajax.request( diff --git a/app/assets/javascripts/app/lib/app_post/interval.coffee b/app/assets/javascripts/app/lib/app_post/interval.coffee index cc341ec41..74c021f80 100644 --- a/app/assets/javascripts/app/lib/app_post/interval.coffee +++ b/app/assets/javascripts/app/lib/app_post/interval.coffee @@ -1,10 +1,10 @@ class App.Interval _instance = undefined - @set: (callback, timeout, key, level) -> + @set: (callback, timeout, key, level, queue) -> if _instance == undefined _instance ?= new _intervalSingleton - _instance.set(callback, timeout, key, level) + _instance.set(callback, timeout, key, level, queue) @clear: (key, level) -> if _instance == undefined @@ -21,6 +21,11 @@ class App.Interval _instance ?= new _intervalSingleton _instance.reset() + @count: -> + if _instance == undefined + _instance ?= new _intervalSingleton + _instance.count() + @_all: -> if _instance == undefined _instance ?= new _intervalSingleton @@ -32,7 +37,7 @@ class _intervalSingleton extends Spine.Module constructor: -> @levelStack = {} - set: (callback, timeout, key, level) => + set: (callback, timeout, key, level, queue) => if !level level = '_all' @@ -44,9 +49,15 @@ class _intervalSingleton extends Spine.Module key = Math.floor(Math.random() * 99999) # setTimeout - @log 'debug', 'set', key, timeout, level, callback - callback() - interval_id = setInterval(callback, timeout) + @log 'debug', 'set', key, timeout, level, callback, queue + localCallback = -> + if queue + App.QueueManager.add('interval', callback) + App.QueueManager.run('interval') + else + callback() + localCallback() + interval_id = setInterval(localCallback, timeout) # remember all interval if !@levelStack[level] @@ -91,5 +102,12 @@ class _intervalSingleton extends Spine.Module @levelStack[level] = {} true + count: => + return 0 if !@levelStack + count = 0 + for levelName, levelValue of @levelStack + count += Object.keys(levelValue).length + count + _all: => @levelStack diff --git a/app/assets/javascripts/app/lib/app_post/overview_list_collection.coffee b/app/assets/javascripts/app/lib/app_post/overview_list_collection.coffee index 2831aacbc..b605f5503 100644 --- a/app/assets/javascripts/app/lib/app_post/overview_list_collection.coffee +++ b/app/assets/javascripts/app/lib/app_post/overview_list_collection.coffee @@ -71,7 +71,10 @@ class _Singleton callback: (view, data) => for counter, meta of @callbacks if meta.view is view - meta.callback(data) + callback = -> + meta.callback(data) + App.QueueManager.add('ticket_overviews', callback) + App.QueueManager.run('ticket_overviews') class App.OverviewListCollection _instance = new _Singleton diff --git a/app/assets/javascripts/app/lib/app_post/pretty_date.coffee b/app/assets/javascripts/app/lib/app_post/pretty_date.coffee index 9c607eee0..13c6157a2 100644 --- a/app/assets/javascripts/app/lib/app_post/pretty_date.coffee +++ b/app/assets/javascripts/app/lib/app_post/pretty_date.coffee @@ -38,11 +38,16 @@ class App.PrettyDate months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] month = months[created.getMonth()] - # for less than 7 days - if diff < (60 * 60 * 24 * 7) + # for less than 6 days + # weekday HH::MM + if diff < (60 * 60 * 24 * 6) string = "#{App.i18n.translateInline(weekday)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}" - else if diff < (60 * 60 * 24 * 7) * 365 + # if it was this year + # weekday DD. MM HH::MM + else if created.getYear() is current.getYear() string = "#{App.i18n.translateInline(weekday)} #{created.getDate()}. #{App.i18n.translateInline(month)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}" + # if it was the year before + # weekday YYYY-MM-DD HH::MM else string = "#{App.i18n.translateInline(weekday)} #{App.i18n.translateTimestamp(time)}" if escalation diff --git a/app/assets/javascripts/app/lib/app_post/utils.coffee b/app/assets/javascripts/app/lib/app_post/utils.coffee index 5b71dd5a2..2c70db01e 100644 --- a/app/assets/javascripts/app/lib/app_post/utils.coffee +++ b/app/assets/javascripts/app/lib/app_post/utils.coffee @@ -124,10 +124,12 @@ class App.Utils @linkify: (string) -> window.linkify(string) - # htmlEscapedAndLinkified = App.Utils.linkify(rawText) + # htmlEscapedAndPhoneified = App.Utils.phoneify(rawText) @phoneify: (string) -> - string = string.replace(/\s+/g, '') - "tel://#{encodeURIComponent(string)}" + return string if _.isEmpty(string) + string = string.replace(/[^0-9,\+,#,\*]+/g, '') + .replace(/(.)\+/, '$1') + "tel:#{string}" # wrappedText = App.Utils.wrap(rawText, maxLineLength) @wrap: (ascii, max = 82) -> @@ -649,6 +651,7 @@ class App.Utils # textReplaced = App.Utils.replaceTags( template, { user: { firstname: 'Bob', lastname: 'Smith' } } ) @replaceTags: (template, objects) -> template = template.replace( /#\{\s{0,2}(.+?)\s{0,2}\}/g, (index, key) -> + key = key.replace(/<.+?>/g, '') levels = key.split(/\./) dataRef = objects for level in levels diff --git a/app/assets/javascripts/app/lib/base/highlight.pack.js b/app/assets/javascripts/app/lib/base/highlight.pack.js index 302535e3d..fc4c31799 100644 --- a/app/assets/javascripts/app/lib/base/highlight.pack.js +++ b/app/assets/javascripts/app/lib/base/highlight.pack.js @@ -1 +1,2 @@ -!function(e){"undefined"!=typeof exports?e(exports):(window.hljs=e({}),"function"==typeof define&&define.amd&&define("hljs",[],function(){return window.hljs}))}(function(e){function n(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0==t.index}function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}function i(e){var n,t,r,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",t=/\blang(?:uage)?-([\w-]+)\b/i.exec(i))return w(t[1])?t[1]:"no-highlight";for(i=i.split(/\s+/),n=0,r=i.length;r>n;n++)if(w(i[n])||a(i[n]))return i[n]}function o(e,n){var t,r={};for(t in e)r[t]=e[t];if(n)for(t in n)r[t]=n[t];return r}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3==i.nodeType?a+=i.nodeValue.length:1==i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!=r[0].offset?e[0].offset"}function u(e){l+=""}function c(e){("start"==e.event?o:u)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g==e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g==e&&g.length&&g[0].offset==s);f.reverse().forEach(o)}else"start"==g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var u={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");u[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):Object.keys(a.k).forEach(function(e){c(e,a.k[e])}),a.k=u}a.lR=t(a.l||/\b\w+\b/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"==e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function l(e,t,a,i){function o(e,n){for(var t=0;t";return i+=e+'">',i+n+o}function p(){if(!L.k)return n(M);var e="",t=0;L.lR.lastIndex=0;for(var r=L.lR.exec(M);r;){e+=n(M.substr(t,r.index-t));var a=g(L,r);a?(B+=a[1],e+=h(a[0],n(r[0]))):e+=n(r[0]),t=L.lR.lastIndex,r=L.lR.exec(M)}return e+n(M.substr(t))}function d(){var e="string"==typeof L.sL;if(e&&!x[L.sL])return n(M);var t=e?l(L.sL,M,!0,y[L.sL]):f(M,L.sL.length?L.sL:void 0);return L.r>0&&(B+=t.r),e&&(y[L.sL]=t.top),h(t.language,t.value,!1,!0)}function b(){return void 0!==L.sL?d():p()}function v(e,t){var r=e.cN?h(e.cN,"",!0):"";e.rB?(k+=r,M=""):e.eB?(k+=n(t)+r,M=""):(k+=r,M=t),L=Object.create(e,{parent:{value:L}})}function m(e,t){if(M+=e,void 0===t)return k+=b(),0;var r=o(t,L);if(r)return k+=b(),v(r,t),r.rB?0:t.length;var a=u(L,t);if(a){var i=L;i.rE||i.eE||(M+=t),k+=b();do L.cN&&(k+=""),B+=L.r,L=L.parent;while(L!=a.parent);return i.eE&&(k+=n(t)),M="",a.starts&&v(a.starts,""),i.rE?0:t.length}if(c(t,L))throw new Error('Illegal lexeme "'+t+'" for mode "'+(L.cN||"")+'"');return M+=t,t.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,L=i||N,y={},k="";for(R=L;R!=N;R=R.parent)R.cN&&(k=h(R.cN,"",!0)+k);var M="",B=0;try{for(var C,j,I=0;;){if(L.t.lastIndex=I,C=L.t.exec(t),!C)break;j=m(t.substr(I,C.index-I),C[0]),I=C.index+j}for(m(t.substr(I)),R=L;R.parent;R=R.parent)R.cN&&(k+="");return{r:B,value:k,language:e,top:L}}catch(O){if(-1!=O.message.indexOf("Illegal"))return{r:0,value:n(t)};throw O}}function f(e,t){t=t||E.languages||Object.keys(x);var r={r:0,value:n(e)},a=r;return t.forEach(function(n){if(w(n)){var t=l(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}}),a.language&&(r.second_best=a),r}function g(e){return E.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,n){return n.replace(/\t/g,E.tabReplace)})),E.useBR&&(e=e.replace(/\n/g,"
")),e}function h(e,n,t){var r=n?R[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function p(e){var n=i(e);if(!a(n)){var t;E.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var r=t.textContent,o=n?l(n,r,!0):f(r),s=u(t);if(s.length){var p=document.createElementNS("http://www.w3.org/1999/xhtml","div");p.innerHTML=o.value,o.value=c(s,u(p),r)}o.value=g(o.value),e.innerHTML=o.value,e.className=h(e.className,n,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function d(e){E=o(E,e)}function b(){if(!b.called){b.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",b,!1),addEventListener("load",b,!1)}function m(n,t){var r=x[n]=t(e);r.aliases&&r.aliases.forEach(function(e){R[e]=n})}function N(){return Object.keys(x)}function w(e){return e=(e||"").toLowerCase(),x[e]||x[R[e]]}var E={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},x={},R={};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=g,e.highlightBlock=p,e.configure=d,e.initHighlighting=b,e.initHighlightingOnLoad=v,e.registerLanguage=m,e.listLanguages=N,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",a={cN:"function",b:c+"\\(",rB:!0,eE:!0,e:"\\("},r={cN:"rule",b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{cN:"value",eW:!0,eE:!0,c:[a,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"id",b:/\#[A-Za-z0-9_-]+/},{cN:"class",b:/\.[A-Za-z0-9_-]+/},{cN:"attr_selector",b:/\[/,e:/\]/,i:"$"},{cN:"pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"']+/},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[a,e.ASM,e.QSM,e.CSSNM]}]},{cN:"tag",b:c,r:0},{cN:"rules",b:"{",e:"}",i:/\S/,c:[e.CBCM,r]}]}});hljs.registerLanguage("scss",function(e){var t="[a-zA-Z-][a-zA-Z0-9_-]*",i={cN:"variable",b:"(\\$"+t+")\\b"},r={cN:"function",b:t+"\\(",rB:!0,eE:!0,e:"\\("},o={cN:"hexcolor",b:"#[0-9A-Fa-f]+"};({cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{cN:"value",eW:!0,eE:!0,c:[r,o,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"important",b:"!important"}]}});return{cI:!0,i:"[=/|']",c:[e.CLCM,e.CBCM,r,{cN:"id",b:"\\#[A-Za-z0-9_-]+",r:0},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"tag",b:"\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b",r:0},{cN:"pseudo",b:":(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)"},{cN:"pseudo",b:"::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)"},i,{cN:"attribute",b:"\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b",i:"[^\\s]"},{cN:"value",b:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{cN:"value",b:":",e:";",c:[r,i,o,e.CSSNM,e.QSM,e.ASM,{cN:"important",b:"!important"}]},{cN:"at_rule",b:"@",e:"[{;]",k:"mixin include extend for if else each while charset import debug media page content font-face namespace warn",c:[r,i,e.QSM,e.ASM,o,e.CSSNM,{cN:"preprocessor",b:"\\s[A-Za-z0-9_.-]+",r:0}]}]}});hljs.registerLanguage("xml",function(t){var s="[A-Za-z0-9\\._:-]+",c={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php"},e={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},t.C("",{r:10}),{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[e],starts:{e:"",rE:!0,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[e],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars"]}},c,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"title",b:/[^ \/><\n\t]+/,r:0},e]}]}});hljs.registerLanguage("ruby",function(e){var c="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r="and false then defined module in return redo if BEGIN retry end for true self when next until do begin unless END rescue nil else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",b={cN:"doctag",b:"@[A-Za-z]+"},a={cN:"value",b:"#<",e:">"},n=[e.C("#","$",{c:[b]}),e.C("^\\=begin","^\\=end",{c:[b],r:10}),e.C("^__END__","\\n$")],s={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,s],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},i={cN:"params",b:"\\(",e:"\\)",k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+e.IR+"::)?"+e.IR}]}].concat(n)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:c}),i].concat(n)},{cN:"constant",b:"(::)?(\\b[A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":",c:[t,{b:c}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{b:"("+e.RSR+")\\s*",c:[a,{cN:"regexp",c:[e.BE,s],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(n),r:0}].concat(n);s.c=d,i.c=d;var o="[>?]>",l="[\\w#]+\\(\\w+\\):\\d+:\\d+>",u="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",N=[{b:/^\s*=>/,cN:"status",starts:{e:"$",c:d}},{cN:"prompt",b:"^("+o+"|"+l+"|"+u+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:n.concat(N).concat(d)}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},t=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{cN:"property",b:"@"+n},{b:"`",e:"`",eB:!0,eE:!0,sL:"javascript"}];r.c=t;var s=e.inherit(e.TM,{b:n}),i="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(t)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:t.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+i,e:"[-=]>",rB:!0,c:[s,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:i,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[s]},s]},{cN:"attribute",b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("javascript",function(e){return{aliases:["js"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"pi",r:10,b:/^\s*['"]use (strict|asm)['"]/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b:/\s*[);\]]/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{bK:"import",e:"[;$]",k:"import from as",c:[e.ASM,e.QSM]},{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]}],i:/#/}}); \ No newline at end of file +/*! highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */ +!function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=B.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function u(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function c(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function u(e){s+=""}function c(e){("start"===e.event?o:u)(e.node)}for(var l=0,s="",f=[];e.length||r.length;){var g=i();if(s+=n(a.substring(l,g[0].offset)),l=g[0].offset,g===e){f.reverse().forEach(u);do c(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===l);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),c(g.splice(0,1)[0])}return s+n(a.substr(l))}function l(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function s(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):x(a.k).forEach(function(e){u(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return l("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var c=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=c.length?t(c.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function c(e,n){return!a&&r(n.iR,e)}function l(e,n){var t=N.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function p(e,n,t,r){var a=r?"":I.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=l(E,r),e?(B+=e[1],a+=p(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function d(){var e="string"==typeof E.sL;if(e&&!y[E.sL])return n(k);var t=e?f(E.sL,k,!0,x[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(B+=t.r),e&&(x[E.sL]=t.top),p(t.language,t.value,!1,!0)}function b(){L+=null!=E.sL?d():h(),k=""}function v(e){L+=e.cN?p(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function m(e,n){if(k+=e,null==n)return b(),0;var t=o(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),b(),t.rB||t.eB||(k=n)),v(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),b(),a.eE&&(k=n));do E.cN&&(L+=C),E.skip||(B+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&v(r.starts,""),a.rE?0:n.length}if(c(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var N=w(e);if(!N)throw new Error('Unknown language: "'+e+'"');s(N);var R,E=i||N,x={},L="";for(R=E;R!==N;R=R.parent)R.cN&&(L=p(R.cN,"",!0)+L);var k="",B=0;try{for(var M,j,O=0;;){if(E.t.lastIndex=O,M=E.t.exec(t),!M)break;j=m(t.substring(O,M.index),M[0]),O=M.index+j}for(m(t.substr(O)),R=E;R.parent;R=R.parent)R.cN&&(L+=C);return{r:B,value:L,language:e,top:E}}catch(T){if(T.message&&-1!==T.message.indexOf("Illegal"))return{r:0,value:n(t)};throw T}}function g(e,t){t=t||I.languages||x(y);var r={r:0,value:n(e)},a=r;return t.filter(w).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return I.tabReplace||I.useBR?e.replace(M,function(e,n){return I.useBR&&"\n"===e?"
":I.tabReplace?n.replace(/\t/g,I.tabReplace):""}):e}function h(e,n,t){var r=n?L[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function d(e){var n,t,r,o,l,s=i(e);a(s)||(I.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,l=n.textContent,r=s?f(s,l,!0):g(l),t=u(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=c(t,u(o),l)),r.value=p(r.value),e.innerHTML=r.value,e.className=h(e.className,s,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){I=o(I,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,d)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=y[n]=t(e);r.aliases&&r.aliases.forEach(function(e){L[e]=n})}function R(){return x(y)}function w(e){return e=(e||"").toLowerCase(),y[e]||y[L[e]]}var E=[],x=Object.keys,y={},L={},k=/^(no-?highlight|plain|text)$/i,B=/\blang(?:uage)?-([\w-]+)\b/i,M=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,C="
",I={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=d,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<(-?)\w+$/,e:/^\s*\w+$/}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+"|unless)\\s*",k:"unless",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d,i.c=d;var l="[>?]>",o="[\\w#]+\\(\\w+\\):\\d+:\\d+>",u="(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>",w=[{b:/^\s*=>/,starts:{e:"$",c:d}},{cN:"meta",b:"^("+l+"|"+o+"|"+u+")",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:s.concat(w).concat(d)}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b://,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}}); \ No newline at end of file diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js index 2a1994828..a82a036dc 100644 --- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js +++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js @@ -88,7 +88,7 @@ // handle enter this.$element.on('keydown', function (e) { _this.log('keydown', e.keyCode) - if ( _this.preventInput ) { + if (_this.preventInput) { this.log('preventInput', _this.preventInput) return } @@ -97,18 +97,29 @@ if (e.keyCode === 13) { // disbale multi line - if ( !_this.options.multiline ) { + if (!_this.options.multiline) { e.preventDefault() return } // break
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 @@
-
+
+
+
<%- @T('Send') %>
\ 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 @@
<% 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 %> -
+
<% for item in @items: %>
<%- @Icon(item.icon) %> diff --git a/app/assets/javascripts/app/views/generic/user_permission.jst.eco b/app/assets/javascripts/app/views/generic/user_permission.jst.eco index b8dd7fd64..64569a55d 100644 --- a/app/assets/javascripts/app/views/generic/user_permission.jst.eco +++ b/app/assets/javascripts/app/views/generic/user_permission.jst.eco @@ -18,7 +18,7 @@ checked<% end %>/> <%- @Icon('checkbox', 'icon-unchecked') %> <%- @Icon('checkbox-checked', 'icon-checked') %> - <%= role.displayName() %> <% if role.note: %>- <%= role.note %><% end %> + <%- @T(role.displayName()) %> <% if role.note: %>- <%- @T(role.note) %><% end %> <% if role.permissions: %> <% for permission in role.permissions: %> diff --git a/app/assets/javascripts/app/views/getting_started/email.jst.eco b/app/assets/javascripts/app/views/getting_started/email.jst.eco index e3ef082f8..1d2d0cb50 100644 --- a/app/assets/javascripts/app/views/getting_started/email.jst.eco +++ b/app/assets/javascripts/app/views/getting_started/email.jst.eco @@ -63,7 +63,7 @@
-
+

<%- @T('Email Inbound') %>

diff --git a/app/assets/javascripts/app/views/import/zendesk.jst.eco b/app/assets/javascripts/app/views/import/zendesk.jst.eco index 72d538d19..eeb7f95c9 100644 --- a/app/assets/javascripts/app/views/import/zendesk.jst.eco +++ b/app/assets/javascripts/app/views/import/zendesk.jst.eco @@ -61,6 +61,7 @@

<%- @T('%s Migration', 'Zendesk') %>

+
diff --git a/app/assets/javascripts/app/views/integration/base.jst.eco b/app/assets/javascripts/app/views/integration/base.jst.eco index 272620104..28b3996bb 100644 --- a/app/assets/javascripts/app/views/integration/base.jst.eco +++ b/app/assets/javascripts/app/views/integration/base.jst.eco @@ -10,9 +10,10 @@
<% if @description: %> <% for item in @description: %> -

<%- @T(item[0], item[1], item[2]) %>

+

<%- @T(item...) %>

<% end %> <% end %>
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/integration/exchange.jst.eco b/app/assets/javascripts/app/views/integration/exchange.jst.eco new file mode 100644 index 000000000..69385e1cf --- /dev/null +++ b/app/assets/javascripts/app/views/integration/exchange.jst.eco @@ -0,0 +1,71 @@ +
+
+

<%- @T('No %s configured.', 'Exchange') %>

+ +
+
+

<%- @T('Settings') %>

+
+ + + + + + + + + +
<%- @T('Name') %> + <%- @T('Value') %> +
<%- @T('Endpoint') %> + <%= @config.endpoint %> +
<%- @T('User') %> + <%= @config.user %> +
<%- @T('Password') %> + <%= @M(@config.password) %> +
+ +

<%- @T('Mapping') %>

+ +

<%- @T('Folders') %>

+ <% if _.isEmpty(@folders): %> + +
<%- @T('No Entries') %> +
+ <% else: %> + + + + + + +
<%- @T('Folder') %> + <% for folder_name in @folders: %> +
<%= folder_name %> + <% end %> +
+ <% end %> + +

<%- @T('User') %>

+ <% if _.isEmpty(@config.attributes): %> + +
<%- @T('No Entries') %> +
+ <% else: %> + + + + + + +
<%- @T('Exchange') %> + <%- @T('Zammad') %> + <% for key, value of @config.attributes: %> +
<%= key %> + <%= value %> + <% end %> +
+ <% end %> + + +
diff --git a/app/assets/javascripts/app/views/integration/exchange_last_import.jst.eco b/app/assets/javascripts/app/views/integration/exchange_last_import.jst.eco new file mode 100644 index 000000000..8323972ad --- /dev/null +++ b/app/assets/javascripts/app/views/integration/exchange_last_import.jst.eco @@ -0,0 +1,54 @@ +
+

<%- @T('Last sync') %>

+ <% if _.isEmpty(@job.started_at): %> + <% if @job.result && @job.result.error: %> + + <% else if @job.result && @job.result.info: %> + + <% else: %> +

<%- @T('Job is waiting to get started...') %>

+ <% end %> + <% else: %> + <% if @job.finished_at: %> +

<%- @Ttimestamp(@job.started_at) %> - <%- @Ttimestamp(@job.finished_at) %>

+ <% if @job.result && @job.result.error: %> + + <% else if @job.result && @job.result.info: %> + + <% end %> + <% else: %> + <% if @job.result && @job.result.error: %> +

<%- @Ttimestamp(@job.started_at) %>

+ + <% else if !@countDone: %> +

<%- @Ttimestamp(@job.started_at) %> - <%- @T('Counting entries. This may take a while.') %>

+ <% else: %> +

<%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %>

+
+ +
+ <% end %> + <% end %> + <% if !_.isEmpty(@job.result) && @countDone: %> +
    +
  • <%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>): +
      +
    • <%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %> +
    +
  • + <% if !_.isEmpty(@job.result.folders): %> +
  • <%- @T('%s folders', 'Exchange') %>: +
      + <% for folder, result of @job.result.folders: %> +
    • <%- folder %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %> + <% end %> +
    +
  • + <% end %> +
+ <% end %> + <% if @job.finished_at: %> + + <% end %> + <% end %> +
diff --git a/app/assets/javascripts/app/views/integration/exchange_summary.jst.eco b/app/assets/javascripts/app/views/integration/exchange_summary.jst.eco new file mode 100644 index 000000000..0abf47bdf --- /dev/null +++ b/app/assets/javascripts/app/views/integration/exchange_summary.jst.eco @@ -0,0 +1,16 @@ +
    +
  • <%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>): +
      +
    • <%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %> +
    +
  • + <% if !_.isEmpty(@job.result.folders): %> +
  • <%- @T('%s folders', 'Exchange') %>: +
      + <% for folder, result of @job.result.folders: %> +
    • <%- folder %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %> + <% end %> +
    +
  • + <% end %> +
diff --git a/app/assets/javascripts/app/views/integration/exchange_user_attribute_row.jst.eco b/app/assets/javascripts/app/views/integration/exchange_user_attribute_row.jst.eco new file mode 100644 index 000000000..1d9dd1826 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/exchange_user_attribute_row.jst.eco @@ -0,0 +1,7 @@ + + + + +
+ <%- @Icon('trash') %> <%- @T('Remove') %> +
diff --git a/app/assets/javascripts/app/views/integration/exchange_wizard.jst.eco b/app/assets/javascripts/app/views/integration/exchange_wizard.jst.eco new file mode 100644 index 000000000..d1b5dc25f --- /dev/null +++ b/app/assets/javascripts/app/views/integration/exchange_wizard.jst.eco @@ -0,0 +1,258 @@ + diff --git a/app/assets/javascripts/app/views/integration/idoit.jst.eco b/app/assets/javascripts/app/views/integration/idoit.jst.eco new file mode 100644 index 000000000..deec7f27f --- /dev/null +++ b/app/assets/javascripts/app/views/integration/idoit.jst.eco @@ -0,0 +1,25 @@ +
+

<%- @T('Settings') %>

+
+ + + + + + + + + +
<%- @T('Name') %> + <%- @T('Value') %> +
<%- @T('API token') %> * + +
<%- @T('Endpoint') %> * + +
<%- @T('Client ID') %> + +
+
+ + +
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/integration/idoit_object_result.jst.eco b/app/assets/javascripts/app/views/integration/idoit_object_result.jst.eco new file mode 100644 index 000000000..e2d4dbe76 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/idoit_object_result.jst.eco @@ -0,0 +1,25 @@ +
+<% if _.isEmpty(@items): %> + <%- @T('none') %> +<% else: %> + + + + + + + + + + <% for item in @items: %> + + + + + + + + <% end %> + +
<%- @T('ID') %><%- @T('Name') %><%- @T('Status') %><%- @T('Link') %>
<%= item.id %><%= item.title %><%= item.cmdb_status_title %>i-doit
+<% end %> diff --git a/app/assets/javascripts/app/views/integration/idoit_object_selector.jst.eco b/app/assets/javascripts/app/views/integration/idoit_object_selector.jst.eco new file mode 100644 index 000000000..fa63a3604 --- /dev/null +++ b/app/assets/javascripts/app/views/integration/idoit_object_selector.jst.eco @@ -0,0 +1,5 @@ + +
diff --git a/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco b/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco index 7fc1e34c6..50ff41cc4 100644 --- a/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco @@ -51,6 +51,10 @@
+
+ + +
diff --git a/app/assets/javascripts/app/views/layout_ref/user_list.jst.eco b/app/assets/javascripts/app/views/layout_ref/user_list.jst.eco index 93f91446e..07f1630aa 100644 --- a/app/assets/javascripts/app/views/layout_ref/user_list.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/user_list.jst.eco @@ -116,8 +116,8 @@
- <%- @Icon('magnifier') %> + <%- @Icon('magnifier') %>
diff --git a/app/assets/javascripts/app/views/login.jst.eco b/app/assets/javascripts/app/views/login.jst.eco index 00b086f48..135c1c859 100644 --- a/app/assets/javascripts/app/views/login.jst.eco +++ b/app/assets/javascripts/app/views/login.jst.eco @@ -24,7 +24,7 @@
- +
diff --git a/app/assets/javascripts/app/views/profile/out_of_office.jst.eco b/app/assets/javascripts/app/views/profile/out_of_office.jst.eco new file mode 100644 index 000000000..e2ca357d2 --- /dev/null +++ b/app/assets/javascripts/app/views/profile/out_of_office.jst.eco @@ -0,0 +1,34 @@ + + +
+
+
+
+

+ <% if @localData.out_of_office is true: %> + <%- @Icon('status', 'ok inline') %> + <% else: %> + <%- @Icon('status', 'error inline') %> + <% end %>

+
+
+
+
<%- @T('From') %>
+
+
+
+
<%- @T('Till') %>
+
+
+
+ + +
+
+
<%- @Ti('Disable') %>
+
<%- @Ti('Enable') %>
+
+
+
diff --git a/app/assets/javascripts/app/views/search/index.jst.eco b/app/assets/javascripts/app/views/search/index.jst.eco index 504e997f3..55637065d 100644 --- a/app/assets/javascripts/app/views/search/index.jst.eco +++ b/app/assets/javascripts/app/views/search/index.jst.eco @@ -3,8 +3,8 @@