-
-
- <%- @Icon('received-calls', 'tab-icon') %> <%- @T('Received Call') %> -
- <%- @Icon('outbound-calls', 'tab-icon') %> <%- @T('Outbound Call') %> - -
- +
- <%- @Icon('email', 'tab-icon') %> <%- @T('Send Email') %>
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index b7776694b..58570b4f9 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,18 @@ ### Infos: -* Used Zammad version: -* Used Zammad installation source: (source, package, ...) -* Operating system: -* Browser + version: +* Used Zammad version: +* Installation method (source, package, ..): +* Operating system: +* Database + version: +* Elasticsearch version: +* Browser + version: ### Expected behavior: -* +* ### Actual behavior: -* +* ### Steps to reproduce the behavior: -* +* +Yes I'm sure this is a bug and no feature request or a general question. diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2e89ebccc..cd85b1971 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -310,7 +310,8 @@ test:integration:es_mysql: - ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/controllers/form_controller_test.rb - - ruby -I test/ test/controllers/user_organization_controller_test.rb + - ruby -I test/ test/controllers/user_controller_test.rb + - ruby -I test/ test/controllers/organization_controller_test.rb - rake db:drop test:integration:es_postgresql: @@ -328,7 +329,8 @@ test:integration:es_postgresql: - ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/controllers/form_controller_test.rb - - ruby -I test/ test/controllers/user_organization_controller_test.rb + - ruby -I test/ test/controllers/user_controller_test.rb + - ruby -I test/ test/controllers/organization_controller_test.rb - rake db:drop test:integration:zendesk_mysql: @@ -355,24 +357,36 @@ test:integration:zendesk_postgresql: - ruby -I test/ test/integration/zendesk_import_test.rb - rake db:drop -test:integration:otrs_5_mysql: +test:integration:otrs_6_mysql: stage: test tags: - core - mysql script: - export RAILS_ENV=test - - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" + - export IMPORT_OTRS_ENDPOINT="http://vz1185.test.znuny.com/otrs/public.pl?Action=ZammadMigrator" - rake db:create - rake db:migrate - ruby -I test/ test/integration/otrs_import_test.rb - rake db:drop -test:integration:otrs_5_postgresql: +test:integration:otrs_6_postgresql: stage: test tags: - core - postgresql + script: + - export RAILS_ENV=test + - export IMPORT_OTRS_ENDPOINT="http://vz1185.test.znuny.com/otrs/public.pl?Action=ZammadMigrator" + - rake db:create + - rake db:migrate + - ruby -I test/ test/integration/otrs_import_test.rb + - rake db:drop + +test:integration:otrs_5: + stage: test + tags: + - core script: - export RAILS_ENV=test - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" diff --git a/.rubocop.yml b/.rubocop.yml index f9156d529..6e91d3296 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -45,29 +45,29 @@ Style/TrailingCommaInArguments: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas' Enabled: false -Style/SpaceInsideParens: +Layout/SpaceInsideParens: Description: 'No spaces after ( or before ).' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' Enabled: false -Style/SpaceAfterMethodName: +Layout/SpaceAfterMethodName: Description: >- Do not put a space between a method name and the opening parenthesis in a method definition. StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' Enabled: false -Style/LeadingCommentSpace: +Layout/LeadingCommentSpace: Description: 'Comments should start with a space.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' Enabled: false -Style/MethodCallParentheses: +Style/MethodCallWithoutArgsParentheses: Description: 'Do not use parentheses for method calls with no arguments.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' Enabled: false -Style/SpaceInsideBrackets: +Layout/SpaceInsideBrackets: Description: 'No spaces after [ or before ].' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' Enabled: false @@ -83,19 +83,19 @@ Style/MethodDefParentheses: StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' Enabled: false -Style/EmptyLinesAroundClassBody: +Layout/EmptyLinesAroundClassBody: Description: "Keeps track of empty lines around class bodies." Enabled: false -Style/EmptyLinesAroundMethodBody: +Layout/EmptyLinesAroundMethodBody: Description: "Keeps track of empty lines around method bodies." Enabled: false -Style/EmptyLinesAroundBlockBody: +Layout/EmptyLinesAroundBlockBody: Description: "Keeps track of empty lines around block bodies." Enabled: false -Style/EmptyLinesAroundModuleBody: +Layout/EmptyLinesAroundModuleBody: Description: "Keeps track of empty lines around module bodies." Enabled: false @@ -143,17 +143,29 @@ Rails/HasAndBelongsToMany: # StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through' Enabled: false +Rails/SkipsModelValidations: + Description: >- + Use methods that skips model validations with caution. + See reference for more information. + Reference: 'http://guides.rubyonrails.org/active_record_validations.html#skipping-validations' + Enabled: true + Exclude: + - test/**/* + Style/ClassAndModuleChildren: Description: 'Checks style of children classes and modules.' Enabled: false -Style/FileName: +Naming/FileName: Description: 'Use snake_case for source file names.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' Enabled: true Exclude: - 'script/websocket-server.rb' +Naming/VariableNumber: + Description: 'Use the configured style when numbering variables.' + Enabled: false # 2.0 @@ -184,8 +196,23 @@ Metrics/ModuleLength: Description: 'Avoid modules longer than 100 lines of code.' Enabled: false +Metrics/BlockLength: + Enabled: false + +Lint/RescueWithoutErrorClass: + Enabled: false + +Rails/ApplicationRecord: + Enabled: false + # TODO +Rails/HasManyOrHasOneDependent: + Enabled: false + +Style/DateTime: + Enabled: false + Style/Documentation: Description: 'Document classes and non-namespace modules.' Enabled: false @@ -193,7 +220,7 @@ Style/Documentation: Lint/UselessAssignment: Enabled: false -Style/ExtraSpacing: +Layout/ExtraSpacing: Description: 'Do not use unnecessary spacing.' Enabled: false @@ -215,4 +242,14 @@ Style/NumericPredicate: AutoCorrect: false Enabled: true Exclude: - - "**/*_spec.rb" \ No newline at end of file + - "**/*_spec.rb" + +Lint/AmbiguousBlockAssociation: + Description: >- + Checks for ambiguous block association with method when param passed without + parentheses. + StyleGuide: '#syntax' + Enabled: true + Exclude: + - "**/*_spec.rb" + - "**/*_examples.rb" diff --git a/.ruby-version b/.ruby-version index 005119baa..8e8299dcc 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.1 +2.4.2 diff --git a/.travis.yml b/.travis.yml index e2ba0b4fc..7602977d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ services: - mysql language: ruby rvm: - - 2.4.1 + - 2.4.2 before_install: - git fetch --unshallow - sudo apt-get -qq update @@ -62,3 +62,4 @@ script: after_success: - if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-build.sh; fi - if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-compose-build.sh; fi + - if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-univention-build.sh; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ceaf451..283608e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Change Log -## [2.2.0](https://github.com/zammad/zammad/tree/2.2.0) (2017-xx-xx) -[Full Changelog](https://github.com/zammad/zammad/compare/2.1.0...2.2.0) +## [2.4.0](https://github.com/zammad/zammad/tree/2.4.0) (2018-xx-xx) +[Full Changelog](https://github.com/zammad/zammad/compare/2.3.0...2.4.0) **Implemented enhancements:** diff --git a/Gemfile b/Gemfile index 812e6cdf9..c980c2377 100644 --- a/Gemfile +++ b/Gemfile @@ -1,111 +1,131 @@ source 'https://rubygems.org' -ruby '2.4.1' - +# core - base +ruby '2.4.2' gem 'rails', '5.1.4' -gem 'rails-observers' + +# core - rails additions gem 'activerecord-session_store' - -# Bundle edge Rails instead: -#gem 'rails', :git => 'git://github.com/rails/rails.git' - +gem 'composite_primary_keys' gem 'json' +gem 'rails-observers' -# Supported DBs +# core - application servers +gem 'puma', group: :puma +gem 'unicorn', group: :unicorn + +# core - supported ORMs gem 'activerecord-nulldb-adapter', group: :nulldb gem 'mysql2', group: :mysql gem 'pg', group: :postgres +# core - asynchrous task execution +gem 'daemons' +gem 'delayed_job_active_record' + +# core - websocket +gem 'em-websocket' +gem 'eventmachine' + +# core - password security +gem 'argon2' + +# performance - Memcached +gem 'dalli' + +# asset handling group :assets do - gem 'sass-rails' #, github: 'rails/sass-rails' + # asset handling - coffee-script gem 'coffee-rails' gem 'coffee-script-source' - gem 'sprockets' - - gem 'uglifier' + # asset handling - frontend templating gem 'eco' + + # asset handling - SASS + gem 'sass-rails' + + # asset handling - pipeline + gem 'sprockets' + gem 'uglifier' end gem 'autoprefixer-rails' +# asset handling - javascript execution for e.g. linux +gem 'execjs' +gem 'libv8' +gem 'therubyracer' + +# authentication - provider gem 'doorkeeper' gem 'oauth2' +# authentication - third party gem 'omniauth' -gem 'omniauth-oauth2' gem 'omniauth-facebook' gem 'omniauth-github' gem 'omniauth-gitlab' gem 'omniauth-google-oauth2' gem 'omniauth-linkedin-oauth2' -gem 'omniauth-twitter' gem 'omniauth-microsoft-office365' +gem 'omniauth-oauth2' +gem 'omniauth-twitter' gem 'omniauth-weibo-oauth2' -gem 'twitter' -gem 'telegramAPI' +# channels gem 'koala' -gem 'mail' -gem 'valid_email2' +gem 'telegramAPI' +gem 'twitter' + +# channels - email additions gem 'htmlentities' - +gem 'mail', '2.6.6' gem 'mime-types' +gem 'valid_email2' +# feature - business hours gem 'biz' -gem 'composite_primary_keys' -gem 'delayed_job_active_record' -gem 'daemons' - -gem 'simple-rss' - -# e. g. on linux we need a javascript execution -gem 'libv8' -gem 'execjs' -gem 'therubyracer' - -require 'erb' -require 'yaml' - -gem 'net-ldap' - -# password security -gem 'argon2' +# feature - signature diffing +gem 'diffy' +# feature - excel output gem 'writeexcel' -gem 'icalendar' -gem 'icalendar-recurrence' + +# feature - device logging gem 'browser' +# feature - iCal export +gem 'icalendar' +gem 'icalendar-recurrence' + # integrations -gem 'slack-notifier' gem 'clearbit' +gem 'net-ldap' +gem 'slack-notifier' gem 'zendesk_api' -gem 'viewpoint' -gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git' + +# integrations - exchange gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git' - -# event machine -gem 'eventmachine' -gem 'em-websocket' - -gem 'diffy' +gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git' +gem 'viewpoint' # Gems used only for develop/test and not required # in production environments by default. group :development, :test do + # test frameworks gem 'rspec-rails' gem 'test-unit' - gem 'spring' - gem 'spring-commands-rspec' + + # test DB gem 'sqlite3' # code coverage + gem 'coveralls', require: false gem 'simplecov' gem 'simplecov-rcov' - gem 'coveralls', require: false # UI tests w/ Selenium gem 'selenium-webdriver', '2.53.4' @@ -120,9 +140,9 @@ group :development, :test do gem 'guard-symlink', require: false # code QA + gem 'coffeelint' gem 'pre-commit' gem 'rubocop' - gem 'coffeelint' # changelog generation gem 'github_changelog_generator' @@ -130,17 +150,14 @@ group :development, :test do # Setting ENV for testing purposes gem 'figaro' - # Use Factory Girl for generating random test data - gem 'factory_girl_rails' + # Use Factory Bot for generating random test data + gem 'factory_bot_rails' # mock http calls gem 'webmock' end -gem 'puma', group: :puma -gem 'unicorn', group: :unicorn - -# load onw gem's +# load onw gems for development and testing purposes local_gemfile = File.join(File.dirname(__FILE__), 'Gemfile.local') if File.exist?(local_gemfile) eval_gemfile local_gemfile diff --git a/Gemfile.lock b/Gemfile.lock index 3e2981f49..6821c1c42 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,22 +65,22 @@ GEM addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) arel (8.0.0) - argon2 (1.1.3) + argon2 (1.1.4) ffi (~> 1.9) ffi-compiler (~> 0.1) ast (2.3.0) - autoprefixer-rails (7.1.3) + autoprefixer-rails (7.1.6) execjs biz (1.7.0) clavius (~> 1.0) tzinfo - browser (2.5.1) + browser (2.5.2) buftok (0.2.0) builder (3.2.3) - childprocess (0.7.1) + childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) clavius (1.0.3) - clearbit (0.2.7) + clearbit (0.2.8) nestful (~> 1.1.0) coderay (1.1.2) coffee-rails (4.2.2) @@ -90,22 +90,24 @@ GEM coffee-script-source execjs coffee-script-source (1.12.2) - coffeelint (1.16.0) + coffeelint (1.16.1) coffee-script execjs json - composite_primary_keys (10.0.0) + composite_primary_keys (10.0.1) activerecord (~> 5.1.0) concurrent-ruby (1.0.5) - coveralls (0.8.21) - json (>= 1.8, < 3) - simplecov (~> 0.14.1) - term-ansicolor (~> 1.3) - thor (~> 0.19.4) - tins (~> 1.6) + coveralls (0.7.1) + multi_json (~> 1.3) + rest-client + simplecov (>= 0.7) + term-ansicolor + thor crack (0.4.3) safe_yaml (~> 1.0.0) - daemons (1.2.4) + crass (1.0.3) + daemons (1.2.5) + dalli (2.7.6) delayed_job (4.1.3) activesupport (>= 3.0, < 5.2) delayed_job_active_record (4.1.2) @@ -127,15 +129,15 @@ GEM eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) equalizer (0.0.11) - erubi (1.6.1) + erubi (1.7.0) eventmachine (1.2.5) execjs (2.7.0) - factory_girl (4.8.0) + factory_bot (4.8.2) activesupport (>= 3.0.0) - factory_girl_rails (4.8.0) - factory_girl (~> 4.8.0) + factory_bot_rails (4.8.2) + factory_bot (~> 4.8.2) railties (>= 3.0.0) - faraday (0.11.0) + faraday (0.12.2) multipart-post (>= 1.2, < 3) faraday-http-cache (2.0.0) faraday (~> 0.8) @@ -154,7 +156,7 @@ GEM rainbow (>= 2.1) rake (>= 10.0) retriable (~> 2.1) - globalid (0.4.0) + globalid (0.4.1) activesupport (>= 4.2.0) guard (2.14.1) formatador (>= 0.2.4) @@ -174,20 +176,21 @@ GEM guard-symlink (0.1.1) guard guard-compat (~> 1.1) - hashdiff (0.3.6) + hashdiff (0.3.7) hashie (3.5.6) htmlentities (4.3.4) - http (2.2.2) + http (3.0.0) addressable (~> 2.3) http-cookie (~> 1.0) - http-form_data (~> 1.0.1) + http-form_data (>= 2.0.0.pre.pre2, < 3) http_parser.rb (~> 0.6.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (1.0.3) + http-form_data (2.0.0) http_parser.rb (0.6.0) httpclient (2.8.3) - i18n (0.8.6) + i18n (0.9.1) + concurrent-ruby (~> 1.0) icalendar (2.4.1) icalendar-recurrence (1.1.2) icalendar (~> 2.0) @@ -210,25 +213,26 @@ GEM logging (2.2.2) little-plugger (~> 1.1) multi_json (~> 1.10) - loofah (2.0.3) + loofah (2.1.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.0.12) mail (2.6.6) mime-types (>= 1.16, < 4) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) - method_source (0.8.2) + method_source (0.9.0) mime-types (2.99.3) mini_portile2 (2.3.0) minitest (5.10.3) multi_json (1.12.2) multi_xml (0.6.0) multipart-post (2.0.0) - mysql2 (0.4.9) + mysql2 (0.4.10) naught (1.1.0) nenv (0.3.0) - nestful (1.1.1) - net-ldap (0.16.0) + nestful (1.1.3) + net-ldap (0.16.1) netrc (0.11.0) nio4r (2.1.0) nokogiri (1.8.1) @@ -246,7 +250,7 @@ GEM rack (>= 1.2, < 3) octokit (4.7.0) sawyer (~> 0.8.0, >= 0.5.3) - omniauth (1.6.1) + omniauth (1.7.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) omniauth-facebook (4.0.0) @@ -280,24 +284,24 @@ GEM omniauth-weibo-oauth2 (0.4.5) omniauth (~> 1.5) omniauth-oauth2 (>= 1.4.0) - parser (2.4.0.0) - ast (~> 2.2) + parallel (1.12.0) + parser (2.4.0.2) + ast (~> 2.3) pg (0.21.0) pluginator (1.5.0) - power_assert (1.1.0) + power_assert (1.1.1) powerpack (0.1.1) - pre-commit (0.35.0) + pre-commit (0.37.0) pluginator (~> 1.5) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - public_suffix (3.0.0) - puma (3.10.0) + method_source (~> 0.9.0) + public_suffix (3.0.1) + puma (3.11.0) rack (2.0.3) rack-livereload (0.3.16) rack - rack-test (0.7.0) + rack-test (0.8.2) rack (>= 1.0, < 3) rails (5.1.4) actioncable (= 5.1.4) @@ -327,7 +331,7 @@ GEM rainbow (2.2.2) rake raindrops (0.19.0) - rake (12.1.0) + rake (12.3.0) rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) @@ -337,39 +341,40 @@ GEM mime-types (>= 1.16, < 3.0) netrc (~> 0.7) retriable (2.1.0) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec-core (3.7.0) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-mocks (3.6.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-rails (3.6.1) + rspec-support (~> 3.7.0) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) - rspec-support (3.6.0) - rubocop (0.42.0) - parser (>= 2.3.1.1, < 3.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) + rubocop (0.51.0) + parallel (~> 1.10) + parser (>= 2.3.3.1, < 3.0) powerpack (~> 0.1) - rainbow (>= 1.99.1, < 3.0) + rainbow (>= 2.2.2, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - ruby-progressbar (1.8.1) + ruby-progressbar (1.9.0) ruby_dep (1.5.0) rubyzip (1.2.1) safe_yaml (1.0.4) - sass (3.5.1) + sass (3.5.3) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.6) + sass-rails (5.0.7) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -383,9 +388,8 @@ GEM rubyzip (~> 1.0) websocket (~> 1.0) shellany (0.0.1) - simple-rss (1.3.1) simple_oauth (0.3.1) - simplecov (0.14.1) + simplecov (0.15.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) @@ -393,11 +397,6 @@ GEM simplecov-rcov (0.2.3) simplecov (>= 0.4.1) slack-notifier (2.3.1) - slop (3.6.0) - spring (2.0.2) - activesupport (>= 4.2) - spring-commands-rspec (1.0.4) - spring (>= 0.9.1) sprockets (3.7.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -410,26 +409,27 @@ GEM rest-client (~> 1.7, >= 1.7.3) term-ansicolor (1.6.0) tins (~> 1.0) - test-unit (3.2.5) + test-unit (3.2.6) power_assert therubyracer (0.12.3) libv8 (~> 3.16.14.15) ref - thor (0.19.4) + thor (0.20.0) thread_safe (0.3.6) tilt (2.0.8) - tins (1.15.0) - twitter (6.1.0) - addressable (~> 2.5) + tins (1.15.1) + twitter (6.2.0) + addressable (~> 2.3) buftok (~> 0.2.0) - equalizer (= 0.0.11) - faraday (~> 0.11.0) - http (~> 2.1) + equalizer (~> 0.0.11) + http (~> 3.0) + http-form_data (~> 2.0) http_parser.rb (~> 0.6.0) - memoizable (~> 0.4.2) - naught (~> 1.1) - simple_oauth (~> 0.3.1) - tzinfo (1.2.3) + memoizable (~> 0.4.0) + multipart-post (~> 2.0) + naught (~> 1.0) + simple_oauth (~> 0.3.0) + tzinfo (1.2.4) thread_safe (~> 0.1) uglifier (3.2.0) execjs (>= 0.3.0, < 3) @@ -437,10 +437,10 @@ GEM unf_ext unf_ext (0.0.7.4) unicode-display_width (1.3.0) - unicorn (5.3.0) + unicorn (5.3.1) kgio (~> 2.6) raindrops (~> 0.7) - valid_email2 (2.0.1) + valid_email2 (2.1.0) activemodel (>= 3.2) mail (~> 2.5) viewpoint (1.1.0) @@ -448,16 +448,16 @@ GEM logging nokogiri rubyntlm - webmock (3.0.1) + webmock (3.1.1) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - websocket (1.2.4) + websocket (1.2.5) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) + websocket-extensions (0.1.3) writeexcel (1.0.5) - zendesk_api (1.14.4) + zendesk_api (1.16.0) faraday (~> 0.9) hashie (>= 3.5.2, < 4.0.0) inflection @@ -482,6 +482,7 @@ DEPENDENCIES composite_primary_keys coveralls daemons + dalli delayed_job_active_record diffy doorkeeper @@ -489,7 +490,7 @@ DEPENDENCIES em-websocket eventmachine execjs - factory_girl_rails + factory_bot_rails figaro github_changelog_generator guard @@ -501,7 +502,7 @@ DEPENDENCIES json koala libv8 - mail + mail (= 2.6.6) mime-types mysql2 net-ldap @@ -528,12 +529,9 @@ DEPENDENCIES rubyntlm! sass-rails selenium-webdriver (= 2.53.4) - simple-rss simplecov simplecov-rcov slack-notifier - spring - spring-commands-rspec sprockets sqlite3 telegramAPI @@ -549,7 +547,7 @@ DEPENDENCIES zendesk_api RUBY VERSION - ruby 2.4.1p111 + ruby 2.4.2p198 BUNDLED WITH - 1.15.4 + 1.16.0 diff --git a/README.md b/README.md index e2f8578ce..030acd785 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ Zammad is a web based open source helpdesk/customer support system with many features to manage customer communication via several channels like telephone, facebook, twitter, chat and e-mails. It is distributed under the GNU AFFERO -General Public License (AGPL) and tested on Linux, Solaris, AIX, FreeBSD, -OpenBSD and Mac OS 10.x. Do you receive many e-mails and want to answer them -with a team of agents? +General Public License (AGPL). + +Do you receive many e-mails and want to answer them with a team of agents? You're going to love Zammad! diff --git a/Rakefile b/Rakefile old mode 100644 new mode 100755 diff --git a/VERSION b/VERSION index 1f4ca9020..a13300a9a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.2.x +2.4.x diff --git a/app/assets/javascripts/app/controllers/_application_controller.coffee b/app/assets/javascripts/app/controllers/_application_controller.coffee index c6fc9fa3f..ff9a2f972 100644 --- a/app/assets/javascripts/app/controllers/_application_controller.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller.coffee @@ -700,6 +700,8 @@ class App.ControllerModal extends App.Controller headPrefix: '' shown: true closeOnAnyClick: false + initalFormParams: {} + initalFormParamsIgnore: false events: 'submit form': 'submit' @@ -746,10 +748,10 @@ class App.ControllerModal extends App.Controller centerButtons: @centerButtons leftButtons: @leftButtons ) - modal.find('.modal-body').html content + modal.find('.modal-body').html(content) if !@initRenderingDone @initRenderingDone = true - @html modal + @html(modal) else @$('.modal-dialog').replaceWith(modal) @post() @@ -761,6 +763,8 @@ class App.ControllerModal extends App.Controller @el render: => + @initalFormParamsIgnore = false + if @buttonSubmit is true @buttonSubmit = 'Submit' if @buttonCancel is true @@ -775,19 +779,18 @@ class App.ControllerModal extends App.Controller if @small @el.addClass('modal--small') - @el.modal + @el.modal( keyboard: @keyboard show: true backdrop: @backdrop container: @container - .on - 'show.bs.modal': @onShow - 'shown.bs.modal': @onShown - 'hide.bs.modal': @onClose - 'hidden.bs.modal': => - @onClosed() - $('.modal').remove() - 'dismiss.bs.modal': @onCancel + ).on( + 'show.bs.modal': @localOnShow + 'shown.bs.modal': @localOnShown + 'hide.bs.modal': @localOnClose + 'hidden.bs.modal': @localOnClosed + 'dismiss.bs.modal': @localOnCancel + ) if @closeOnAnyClick @el.on('click', => @@ -797,6 +800,7 @@ class App.ControllerModal extends App.Controller close: (e) => if e e.preventDefault() + @initalFormParamsIgnore = true @el.modal('hide') formParams: => @@ -804,28 +808,50 @@ class App.ControllerModal extends App.Controller return @formParam(@container.find('.modal form')) return @formParam(@$('.modal form')) - onShow: -> + localOnShow: (e) => + @onShow(e) + + onShow: (e) -> # do nothing - onShown: => + localOnShown: (e) => + @onShown(e) + + onShown: (e) => @$('input:not([disabled]):not([type="hidden"]):not(".btn"), textarea').first().focus() + @initalFormParams = @formParams() + + localOnClose: (e) => + diff = difference(@initalFormParams, @formParams()) + if @initalFormParamsIgnore is false && !_.isEmpty(diff) + if !confirm(App.i18n.translateContent('The form content has been changed. Do you want to close it and lose your changes?')) + e.preventDefault() + return + @onClose(e) onClose: -> # do nothing - onClosed: -> + localOnClosed: (e) => + @onClosed(e) + $('.modal').remove() + + onClosed: (e) -> # do nothing - onSubmit: -> - # do nothing + localOnCancel: (e) => + @onCancel(e) - onCancel: -> + onCancel: (e) -> # do nothing cancel: (e) => @close(e) @onCancel(e) + onSubmit: (e) -> + # do nothing + submit: (e) => e.stopPropagation() e.preventDefault() diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.coffee index bc82db034..91fdcbdb1 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_form.coffee @@ -86,6 +86,9 @@ class App.ControllerForm extends App.Controller for attribute in @attributes attribute_count = attribute_count + 1 + if @isDisabled == true + attribute.disabled = true + # add item item = @formGenItem(attribute, className, fieldset, attribute_count) item.appendTo(fieldset) diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee index 71789f94c..9b2c87cc8 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee @@ -148,24 +148,26 @@ class App.ControllerGenericIndex extends App.Controller return item ) - # show description button, only if content exists - showDescription = false - if App[ @genericObject ].description && !_.isEmpty(objects) - showDescription = true + if !@table - @html App.view('generic/admin/index')( - head: @pageData.objects - notes: @pageData.notes - buttons: @pageData.buttons - menus: @pageData.menus - showDescription: showDescription - ) + # show description button, only if content exists + showDescription = false + if App[ @genericObject ].description && !_.isEmpty(objects) + showDescription = true - # show description in content if no no content exists - if _.isEmpty(objects) && App[ @genericObject ].description - description = marked(App[ @genericObject ].description) - @$('.table-overview').html(description) - return + @html App.view('generic/admin/index')( + head: @pageData.objects + notes: @pageData.notes + buttons: @pageData.buttons + menus: @pageData.menus + showDescription: showDescription + ) + + # show description in content if no no content exists + if _.isEmpty(objects) && App[ @genericObject ].description + description = marked(App[ @genericObject ].description) + @$('.table-overview').html(description) + return # append content table params = _.extend( @@ -184,7 +186,10 @@ class App.ControllerGenericIndex extends App.Controller }, @pageData.tableExtend ) - new App.ControllerTable(params) + if !@table + @table = new App.ControllerTable(params) + else + @table.update(objects: objects) edit: (id, e) => e.preventDefault() @@ -651,7 +656,7 @@ class App.Sidebar extends App.Controller '.sidebar': 'sidebars' events: - 'click .tabsSidebar-tab': 'toggleTab' + 'click .tabsSidebar-tab': 'toggleTab' 'click .tabsSidebar-close': 'toggleSidebar' 'click .sidebar-header .js-headline': 'toggleDropdown' @@ -675,26 +680,48 @@ class App.Sidebar extends App.Controller @toggleTabAction(name) render: => + itemsLocal = [] + for item in @items + itemLocal = item.sidebarItem() + if itemLocal + itemsLocal.push itemLocal + + # container localEl = $(App.view('generic/sidebar_tabs')( - items: @items + items: itemsLocal scrollbarWidth: App.Utils.getScrollBarWidth() dir: App.i18n.dir() )) - # init content callback - for item in @items - area = localEl.filter('.sidebar[data-tab="' + item.name + '"]') - if item.callback - item.callback( area.find('.sidebar-content') ) - if item.actions - new App.ActionRow( - el: area.find('.js-actions') - items: item.actions - type: 'small' - ) + # init sidebar badget + for item in itemsLocal + el = localEl.find('.tabsSidebar-tab[data-tab="' + item.name + '"]') + if item.badgeCallback + item.badgeCallback(el) + else + @badgeRender(el, item) + + # init sidebar content + for item in itemsLocal + if item.sidebarCallback + el = localEl.filter('.sidebar[data-tab="' + item.name + '"]') + item.sidebarCallback(el.find('.sidebar-content')) + if !_.isEmpty(item.sidebarActions) + new App.ActionRow( + el: el.find('.js-actions') + items: item.sidebarActions + type: 'small' + ) @html localEl + badgeRender: (el, item) => + @badgeEl = el + @badgeRenderLocal(item) + + badgeRenderLocal: (item) => + @badgeEl.html(App.view('generic/sidebar_tabs_item')(icon: item.badgeIcon)) + toggleDropdown: (e) -> e.stopPropagation() $(e.currentTarget).next('.js-actions').find('.dropdown-toggle').dropdown('toggle') @@ -1170,7 +1197,6 @@ class App.ObserverController extends App.Controller if @globalRerender @bind('ui:rerender', => @lastAttributres = undefined - console.log('aaaa', @model, @template) @maybeRender(App[@model].fullLocal(@object_id)) ) diff --git a/app/assets/javascripts/app/controllers/_application_controller_table.coffee b/app/assets/javascripts/app/controllers/_application_controller_table.coffee index 21fcab3e9..64433f28e 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_table.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_table.coffee @@ -97,6 +97,7 @@ class App.ControllerTable extends App.Controller checkBoxColWidth: 40 radioColWidth: 22 sortableColWidth: 36 + destroyColWidth: 70 elements: '.js-tableHead': 'tableHead' @@ -133,6 +134,8 @@ class App.ControllerTable extends App.Controller customOrderDirection: undefined customOrderBy: undefined + frontendTimeUpdateExecute: true + bindCol: {} bindRow: {} @@ -269,6 +272,7 @@ class App.ControllerTable extends App.Controller @currentRows = newCurrentRows @log 'debug', 'table.fullRender.contentRemoved', removePositions, addPositions @renderPager(@el, true) + @frontendTimeUpdateElement(@el) if @frontendTimeUpdateExecute is true return ['fullRender.contentRemoved', removePositions, addPositions] if newRows.length isnt @currentRows.length @@ -304,6 +308,7 @@ class App.ControllerTable extends App.Controller else @currentRows = clone(rows) container.find('.js-tableBody').html(rows) + @frontendTimeUpdateElement(container) if @frontendTimeUpdateExecute is true @renderPager(container) @@ -506,6 +511,7 @@ class App.ControllerTable extends App.Controller # get header data @headers = [] + availableWidth = @availableWidth for item in @overviewAttributes headerFound = false for attributeName, attribute of @attributesList @@ -520,7 +526,7 @@ class App.ControllerTable extends App.Controller # e.g. column: owner headerFound = true if @headerWidth[attribute.name] - attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth + attribute.displayWidth = @headerWidth[attribute.name] * availableWidth else if !attribute.width attribute.displayWidth = @baseColWidth else @@ -529,7 +535,7 @@ class App.ControllerTable extends App.Controller unit = attribute.width.match(/[px|%]+/)[0] if unit is '%' - attribute.displayWidth = value / 100 * @el.width() + attribute.displayWidth = value / 100 * availableWidth else attribute.displayWidth = value @headers.push attribute @@ -538,7 +544,7 @@ class App.ControllerTable extends App.Controller if attributeName is "#{item}_id" || attributeName is "#{item}_ids" headerFound = true if @headerWidth[attribute.name] - attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth + attribute.displayWidth = @headerWidth[attribute.name] * availableWidth else if !attribute.width attribute.displayWidth = @baseColWidth else @@ -547,7 +553,7 @@ class App.ControllerTable extends App.Controller unit = attribute.width.match(/[px|%]+/)[0] if unit is '%' - attribute.displayWidth = value / 100 * @el.width() + attribute.displayWidth = value / 100 * availableWidth else attribute.displayWidth = value @headers.push attribute @@ -741,8 +747,10 @@ class App.ControllerTable extends App.Controller if @availableWidth is 0 @availableWidth = @minTableWidth + availableWidth = @availableWidth + widths = @getHeaderWidths() - shrinkBy = Math.ceil (widths - @availableWidth) / @getShrinkableHeadersCount() + shrinkBy = Math.ceil (widths - availableWidth) / @getShrinkableHeadersCount() # make all cols evenly smaller @headers = _.map @headers, (col) => @@ -751,7 +759,8 @@ class App.ControllerTable extends App.Controller return col # give left-over space from rounding to last column to get to 100% - roundingLeftOver = @availableWidth - @getHeaderWidths() + roundingLeftOver = availableWidth - @getHeaderWidths() + # but only if there is something left over (will get negative when there are too many columns for each column to stay in their min width) if roundingLeftOver > 0 @headers[@headers.length - 1].displayWidth = @headers[@headers.length - 1].displayWidth + roundingLeftOver @@ -777,6 +786,9 @@ class App.ControllerTable extends App.Controller if @dndCallback widths += @sortableColWidth + if @destroy + widths += @destroyColWidth + widths setHeaderWidths: => diff --git a/app/assets/javascripts/app/controllers/_channel/email.coffee b/app/assets/javascripts/app/controllers/_channel/email.coffee index 5c92bc279..20a827462 100644 --- a/app/assets/javascripts/app/controllers/_channel/email.coffee +++ b/app/assets/javascripts/app/controllers/_channel/email.coffee @@ -478,7 +478,6 @@ class App.ChannelEmailEdit extends App.ControllerModal class App.ChannelEmailAccountWizard extends App.WizardModal elements: '.modal-body': 'body' - events: 'submit .js-intro': 'probeBasedOnIntro' 'submit .js-inbound': 'probeInbound' @@ -487,6 +486,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal 'click .js-goToSlide': 'goToSlide' 'click .js-expert': 'probeBasedOnIntro' 'click .js-close': 'hide' + inboundPassword: '' + outboundPassword: '' + passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}' constructor: -> super @@ -503,9 +505,17 @@ class App.ChannelEmailAccountWizard extends App.WizardModal if @channel @account = - inbound: @channel.options.inbound - outbound: @channel.options.outbound - meta: {} + inbound: clone(@channel.options.inbound) + outbound: clone(@channel.options.outbound) + meta: {} + + # remember passwords, do not show in ui + if @account.inbound.options && @account.inbound.options.password + @inboundPassword = @account.inbound.options.password + @account.inbound.options.password = @passwordPlaceholder + if @account.outbound.options && @account.outbound.options.password + @outboundPassword = @account.outbound.options.password + @account.outbound.options.password = @passwordPlaceholder if @container @el.addClass('modal--local') @@ -515,17 +525,17 @@ class App.ChannelEmailAccountWizard extends App.WizardModal if @channel @$('.js-goToSlide[data-slide=js-intro]').addClass('hidden') - @el.modal + @el.modal( keyboard: true show: true backdrop: true container: @container - .on + ).on( 'hidden.bs.modal': => if @callback @callback() @el.remove() - + ) if @slide @showSlide(@slide) @@ -712,6 +722,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal # get params params = @formParam(e.target) + if params.options.password is @passwordPlaceholder + params.options.password = @inboundPassword + # let backend know about the channel if @channel params.channel_id = @channel.id @@ -771,6 +784,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal params = @formParam(e.target) params['email'] = @account['meta']['email'] + if params.options.password is @passwordPlaceholder + params.options.password = @outboundPassword + if !params['email'] && @channel email_addresses = App.EmailAddress.search(filter: { channel_id: @channel.id }) if email_addresses && email_addresses[0] @@ -867,11 +883,13 @@ class App.ChannelEmailAccountWizard extends App.WizardModal class App.ChannelEmailNotificationWizard extends App.WizardModal elements: '.modal-body': 'body' - events: 'change .js-outbound [name=adapter]': 'toggleOutboundAdapter' 'submit .js-outbound': 'probleOutbound' 'click .js-close': 'hide' + inboundPassword: '' + outboundPassword: '' + passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}' constructor: -> super @@ -888,27 +906,35 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal if @channel @account = - inbound: @channel.options.inbound - outbound: @channel.options.outbound + inbound: clone(@channel.options.inbound) + outbound: clone(@channel.options.outbound) + + # remember passwords, do not show in ui + if @account.inbound && @account.inbound.options && @account.inbound.options.password + @inboundPassword = @account.inbound.options.password + @account.inbound.options.password = @passwordPlaceholder + if @account.outbound && @account.outbound.options && @account.outbound.options.password + @outboundPassword = @account.outbound.options.password + @account.outbound.options.password = @passwordPlaceholder if @container @el.addClass('modal--local') @render() - @el.modal + @el.modal( keyboard: true show: true backdrop: true container: @container - .on + ).on( 'show.bs.modal': @onShow 'shown.bs.modal': @onShown 'hidden.bs.modal': => if @callback @callback() @el.remove() - + ) if @slide @showSlide(@slide) @@ -956,6 +982,9 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal # get params params = @formParam(e.target) + if params.options && params.options.password is @passwordPlaceholder + params.options.password = @outboundPassword + # let backend know about the channel params.channel_id = @channel.id diff --git a/app/assets/javascripts/app/controllers/_channel/form.coffee b/app/assets/javascripts/app/controllers/_channel/form.coffee index 96cca32d3..2edaa3cdc 100644 --- a/app/assets/javascripts/app/controllers/_channel/form.coffee +++ b/app/assets/javascripts/app/controllers/_channel/form.coffee @@ -67,11 +67,15 @@ class App.ChannelForm extends App.ControllerSubContent # rebuild preview params.test = true if params.modal + @$('.js-modal').removeClass('hide') + @$('.js-inlineForm').addClass('hide') @$('.js-formInline').addClass('hide') @$('.js-formBtn').removeClass('hide') @$('.js-formBtn').ZammadForm(params) @$('.js-formBtn').text('Feedback') else + @$('.js-modal').addClass('hide') + @$('.js-inlineForm').removeClass('hide') @$('.js-formBtn').addClass('hide') @$('.js-formInline').removeClass('hide') @$('.js-formInline').ZammadForm(params) diff --git a/app/assets/javascripts/app/controllers/_integration/exchange.coffee b/app/assets/javascripts/app/controllers/_integration/exchange.coffee index c02c541ba..13ea97709 100644 --- a/app/assets/javascripts/app/controllers/_integration/exchange.coffee +++ b/app/assets/javascripts/app/controllers/_integration/exchange.coffee @@ -131,13 +131,12 @@ class Form extends App.Controller 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)) + el = $(App.view('integration/exchange_last_import')(job: job)) @lastImport.html(el) activeDryRun: => @@ -540,34 +539,26 @@ class ConnectionWizard extends App.WizardModal @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) + @$('.js-progress progress').attr('value', job.result.sum) + @$('.js-progress progress').attr('max', job.result.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) + @tryResult(job) else @delay(@tryLoop, 4000) ) - tryResult: (job, total) => + tryResult: (job) => @showSlide('js-try') - el = $(App.view('integration/exchange_summary')(job: job, countDone: total)) + el = $(App.view('integration/exchange_summary')(job: job)) @el.find('.js-summary').html(el) App.Config.set( diff --git a/app/assets/javascripts/app/controllers/_integration/ldap.coffee b/app/assets/javascripts/app/controllers/_integration/ldap.coffee index cd6312a68..433b80b57 100644 --- a/app/assets/javascripts/app/controllers/_integration/ldap.coffee +++ b/app/assets/javascripts/app/controllers/_integration/ldap.coffee @@ -132,13 +132,12 @@ class Form extends App.Controller 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/ldap_last_import')(job: job, countDone: countDone)) + el = $(App.view('integration/ldap_last_import')(job: job)) @lastImport.html(el) activeDryRun: => @@ -419,7 +418,7 @@ class ConnectionWizard extends App.WizardModal if !_.isArray(user_attributes[key]) user_attributes[key] = [user_attributes[key]] user_attributes_local = - "#{@wizardConfig['user_uid']}": 'login' + 'samaccountname': 'login' length = user_attributes.source.length-1 for count in [0..length] if user_attributes.source[count] && user_attributes.dest[count] @@ -450,7 +449,7 @@ class ConnectionWizard extends App.WizardModal buildRowsUserMap: (user_attribute_map) => # show static login row - userUidDisplayValue = @wizardConfig.wizardData.backend_user_attributes[ @wizardConfig['user_uid'] ] + userUidDisplayValue = @wizardConfig.wizardData.backend_user_attributes['samaccountname'] el = [ $(App.view('integration/ldap_user_attribute_row_read_only')( @@ -459,7 +458,7 @@ class ConnectionWizard extends App.WizardModal )) ] for source, dest of user_attribute_map - continue if source == @wizardConfig['user_uid'] + continue if source == 'samaccountname' continue if !(source of @wizardConfig.wizardData.backend_user_attributes) el.push @buildRowUserAttribute(source, dest) el @@ -539,22 +538,12 @@ class ConnectionWizard extends App.WizardModal @showAlert('js-error', (job.result.error || job.result.info)) return - if job.result && job.result.sum + if job.result && job.result.total @$('.js-preprogress').addClass('hide') @$('.js-analyzing').removeClass('hide') - total = 0 - if job.result.created - total += job.result.created - if job.result.failed - total += job.result.failed - if job.result.skipped - total += job.result.skipped - if job.result.unchanged - total += job.result.unchanged - if job.result.updated - total += job.result.updated - @$('.js-progress progress').attr('value', total) - @$('.js-progress progress').attr('max', job.result.sum) + + @$('.js-progress progress').attr('value', job.result.sum) + @$('.js-progress progress').attr('max', job.result.total) if job.finished_at # reset initial state in case the back button is used @$('.js-preprogress').removeClass('hide') @@ -574,9 +563,8 @@ class ConnectionWizard extends App.WizardModal for role_id, statistic of job.result.role_ids role = App.Role.find(role_id) job.result.roles[role.displayName()] = statistic - countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped @showSlide('js-try') - el = $(App.view('integration/ldap_summary')(job: job, countDone: countDone)) + el = $(App.view('integration/ldap_summary')(job: job)) @el.find('.js-summary').html(el) App.Config.set( diff --git a/app/assets/javascripts/app/controllers/_settings/area_item.coffee b/app/assets/javascripts/app/controllers/_settings/area_item.coffee index c7db71eb3..877caeee0 100644 --- a/app/assets/javascripts/app/controllers/_settings/area_item.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area_item.coffee @@ -32,7 +32,7 @@ class App.SettingsAreaItem extends App.Controller ) new App.ControllerForm( - el: @el.find('.form-item'), + el: @el.find('.form-item') model: { configure_attributes: @configure_attributes, className: '' } autofocus: false ) diff --git a/app/assets/javascripts/app/controllers/_settings/area_item_default_locale.coffee b/app/assets/javascripts/app/controllers/_settings/area_item_default_locale.coffee new file mode 100644 index 000000000..5d22c5a59 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_settings/area_item_default_locale.coffee @@ -0,0 +1,21 @@ +class App.SettingsAreaItemDefaultLocale extends App.SettingsAreaItem + + render: => + + options = {} + locales = App.Locale.all() + for locale in locales + options[locale.locale] = locale.name + configure_attributes = [ + { name: 'locale_default', display: '', tag: 'searchable_select', null: false, class: 'input', options: options, default: @setting.state_current.value }, + ] + + @html App.view(@template)( + setting: @setting + ) + + new App.ControllerForm( + el: @el.find('.form-item') + model: { configure_attributes: configure_attributes, className: '' } + autofocus: false + ) diff --git a/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee b/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee index 70ccfb563..dcbd9f6f6 100644 --- a/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area_ticket_number.coffee @@ -42,7 +42,7 @@ class App.SettingsAreaTicketNumber extends App.Controller number = "#{App.Config.get('ticket_hook')}#{App.Config.get('system_id')}" counter = '1' if paramsItem.min_size - minSize = parseInt(paramsItem.min_size) + minSize = parseInt(paramsItem.min_size) - "#{App.Config.get('system_id')}".length if paramsItem.checksum minSize -= 1 if minSize > 1 diff --git a/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee b/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee index e7ab5301e..6741b3946 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/column_select.coffee @@ -6,24 +6,24 @@ class App.UiElement.column_select extends App.UiElement.ApplicationUiElement attribute.multiple = 'multiple' # build options list based on config - @getConfigOptionList( attribute, params ) + @getConfigOptionList(attribute, params) # build options list based on relation - @getRelationOptionList( attribute, params ) + @getRelationOptionList(attribute, params) # add null selection if needed - @addNullOption( attribute, params ) + @addNullOption(attribute, params) # sort attribute.options - @sortOptions( attribute, params ) + @sortOptions(attribute, params) # find selected/checked item of list - @selectedOptions( attribute, params ) + @selectedOptions(attribute, params) # disable item of list - @disabledOptions( attribute, params ) + @disabledOptions(attribute, params) # filter attributes - @filterOption( attribute, params ) + @filterOption(attribute, params) - new App.ColumnSelect( attribute: attribute ).element() + new App.ColumnSelect(attribute: attribute).element() diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee index f3cba6f7b..387a35a8f 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee @@ -1,8 +1,7 @@ # coffeelint: disable=camel_case_classes class App.UiElement.richtext - @render: (attribute) -> - - item = $( App.view('generic/richtext')( attribute: attribute ) ) + @render: (attribute, params) -> + item = $( App.view('generic/richtext')(attribute: attribute) ) item.find('[contenteditable]').ce( mode: attribute.type maxlength: attribute.maxlength @@ -15,42 +14,42 @@ class App.UiElement.richtext new App[plugin.controller](params) if attribute.upload - item.append( $( App.view('generic/attachment')( attribute: attribute ) ) ) + @attachments = [] + item.append( $( App.view('generic/attachment')(attribute: attribute) ) ) - renderAttachment = (file) => - item.find('.attachments').append( App.view('generic/attachment_item')( - fileName: file.filename - fileSize: App.Utils.humanFileSize(file.size) - store_id: file.store_id - )) - item.on( - 'click' - "[data-id=#{file.store_id}]", (e) => - @attachments = _.filter( - @attachments, - (item) -> - return if item.id isnt file.store_id - item - ) - store_id = $(e.currentTarget).data('id') + renderFile = (file) => + item.find('.attachments').append(App.view('generic/attachment_item')(file)) + @attachments.push file - # delete attachment from storage - App.Ajax.request( - type: 'DELETE' - url: "#{App.Config.get('api_path')}/ticket_attachment_upload" - data: JSON.stringify(store_id: store_id), - processData: false - ) + if params && params.attachments + for file in params.attachments + renderFile(file) - # remove attachment from dom - element = $(e.currentTarget).closest('.attachments') - $(e.currentTarget).closest('.attachment').remove() - # empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o - if element.find('.attachment').length == 0 - element.empty() + # remove items + item.find('.attachments').on('click', '.js-delete', (e) => + id = $(e.currentTarget).data('id') + @attachments = _.filter( + @attachments, + (item) -> + return if item.id.toString() is id.toString() + item ) - @attachments = [] + # delete attachment from storage + App.Ajax.request( + type: 'DELETE' + url: "#{App.Config.get('api_path')}/ticket_attachment_upload" + data: JSON.stringify(id: id), + processData: false + ) + + # remove attachment from dom + element = $(e.currentTarget).closest('.attachments') + $(e.currentTarget).closest('.attachment').remove() + if element.find('.attachment').length == 0 + element.empty() + ) + @progressBar = item.find('.attachmentUpload-progressBar') @progressText = item.find('.js-percentage') @attachmentPlaceholder = item.find('.attachmentPlaceholder') @@ -84,7 +83,6 @@ class App.UiElement.richtext # Called after received response from the server onCompleted: (response) => response = JSON.parse(response) - @attachments.push response.data @attachmentPlaceholder.removeClass('hide') @attachmentUpload.addClass('hide') @@ -93,7 +91,7 @@ class App.UiElement.richtext @progressBar.width(parseInt(0) + '%') @progressText.text('') - renderAttachment(response.data) + renderFile(response.data) item.find('input').val('') App.Log.debug 'UiElement.richtext', 'upload complete', response.data @@ -111,4 +109,5 @@ class App.UiElement.richtext ) ) App.Delay.set(u, 100, undefined, 'form_upload') + item diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext_search.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext_search.coffee new file mode 100644 index 000000000..9a5f05581 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/richtext_search.coffee @@ -0,0 +1,4 @@ +# coffeelint: disable=camel_case_classes +class App.UiElement.richtext_search + @render: (attribute) -> + $( App.view('generic/input')( attribute: attribute ) ) diff --git a/app/assets/javascripts/app/controllers/_ui_element/select_search.coffee b/app/assets/javascripts/app/controllers/_ui_element/select_search.coffee new file mode 100644 index 000000000..2b53dd6d1 --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/select_search.coffee @@ -0,0 +1,35 @@ +# coffeelint: disable=camel_case_classes +class App.UiElement.select_search extends App.UiElement.ApplicationUiElement + @render: (attribute, params) -> + + # set multiple option + if attribute.multiple + attribute.multiple = 'multiple' + else + attribute.multiple = '' + + delete attribute.filter + + # build options list based on config + @getConfigOptionList(attribute, params) + + # build options list based on relation + @getRelationOptionList(attribute, params) + + # add null selection if needed + @addNullOption(attribute, params) + + # sort attribute.options + @sortOptions(attribute, params) + + # finde selected/checked item of list + @selectedOptions(attribute, params) + + # disable item of list + @disabledOptions(attribute, params) + + # filter attributes + @filterOption(attribute, params) + + # return item + $( App.view('generic/select')(attribute: attribute) ) 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 a88901169..f46eac253 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee @@ -22,9 +22,12 @@ class App.UiElement.ticket_selector '^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)'] 'boolean$': ['is', 'is not'] + 'integer$': ['is', 'is not'] '^radio$': ['is', 'is not'] '^select$': ['is', 'is not'] + '^tree_select$': ['is', 'is not'] '^input$': ['contains', 'contains not'] + '^richtext$': ['contains', 'contains not'] '^textarea$': ['contains', 'contains not'] '^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not'] @@ -34,9 +37,12 @@ class App.UiElement.ticket_selector '^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed'] 'boolean$': ['is', 'is not', 'has changed'] + 'integer$': ['is', 'is not', 'has changed'] '^radio$': ['is', 'is not', 'has changed'] '^select$': ['is', 'is not', 'has changed'] + '^tree_select$': ['is', 'is not', 'has changed'] '^input$': ['contains', 'contains not', 'has changed'] + '^richtext$': ['contains', 'contains not', 'has changed'] '^textarea$': ['contains', 'contains not', 'has changed'] '^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not'] diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index 617c83f0c..de43df318 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -14,6 +14,8 @@ class App.TicketCreate extends App.Controller # define default type @default_type = 'phone-in' + @formId = App.ControllerForm.formId() + # remember split info if exists @split = '' if @ticket_id && @article_id @@ -92,6 +94,10 @@ class App.TicketCreate extends App.Controller else @$('[name="cc"]').closest('.form-group').addClass('hide') + # show notice + @$('.js-note').addClass('hide') + @$(".js-note[data-type='#{type}']").removeClass('hide') + App.TaskManager.touch(@task_key) meta: => @@ -158,7 +164,7 @@ class App.TicketCreate extends App.Controller # get data / in case also ticket data for split buildScreen: (params) => - if !params.ticket_id && !params.article_id + if _.isEmpty(params.ticket_id) && _.isEmpty(params.article_id) if !_.isEmpty(params.customer_id) @render(options: { customer_id: params.customer_id }) return @@ -173,6 +179,7 @@ class App.TicketCreate extends App.Controller data: ticket_id: params.ticket_id article_id: params.article_id + form_id: @formId processData: true success: (data, status, xhr) => @@ -194,6 +201,9 @@ class App.TicketCreate extends App.Controller else t.body = App.Utils.text2html(a.body) + # add attachments + t.attachments = data.attachments + # render page @render(options: t) ) @@ -201,23 +211,20 @@ class App.TicketCreate extends App.Controller render: (template = {}) -> # get params - params = {} + params = @prefilledParams || {} if template && !_.isEmpty(template.options) params = template.options else if App.TaskManager.get(@task_key) && !_.isEmpty(App.TaskManager.get(@task_key).state) params = App.TaskManager.get(@task_key).state + if !_.isEmpty(params['form_id']) + @formId = params['form_id'] - if params['form_id'] - @form_id = params['form_id'] - else - @form_id = App.ControllerForm.formId() - - @html App.view('agent_ticket_create')( + @html(App.view('agent_ticket_create')( head: 'New Ticket' agent: @permissionCheck('ticket.agent') admin: @permissionCheck('admin') - form_id: @form_id - ) + form_id: @formId + )) signatureChanges = (params, attribute, attributes, classname, form, ui) => if attribute && attribute.name is 'group_id' @@ -272,7 +279,7 @@ class App.TicketCreate extends App.Controller } new App.ControllerForm( el: @$('.ticket-form-top') - form_id: @form_id + form_id: @formId model: App.Ticket screen: 'create_top' events: @@ -288,14 +295,14 @@ class App.TicketCreate extends App.Controller new App.ControllerForm( el: @$('.article-form-top') - form_id: @form_id + form_id: @formId model: App.TicketArticle screen: 'create_top' params: params ) new App.ControllerForm( el: @$('.ticket-form-middle') - form_id: @form_id + form_id: @formId model: App.Ticket screen: 'create_middle' events: @@ -310,7 +317,7 @@ class App.TicketCreate extends App.Controller ) new App.ControllerForm( el: @$('.ticket-form-bottom') - form_id: @form_id + form_id: @formId model: App.Ticket screen: 'create_bottom' events: @@ -420,7 +427,7 @@ class App.TicketCreate extends App.Controller body: params.body type_id: type.id sender_id: sender.id - form_id: @form_id + form_id: @formId content_type: 'text/html' } else @@ -432,7 +439,7 @@ class App.TicketCreate extends App.Controller body: params.body type_id: type.id sender_id: sender.id - form_id: @form_id + form_id: @formId content_type: 'text/html' } diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar.coffee index 88b151449..1d34f85d6 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar.coffee @@ -32,9 +32,7 @@ class App.TicketCreateSidebar extends App.Controller params: @params query: @query ) - item = @sidebarBackends[key].sidebarItem() - if item - @sidebarItems.push item + @sidebarItems.push @sidebarBackends[key] new App.Sidebar( el: @el 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 index cbfd4e7e9..8de9ea04a 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee @@ -1,26 +1,62 @@ class SidebarCustomer extends App.Controller sidebarItem: => return if !@permissionCheck('ticket.agent') - return if !@params.customer_id - { - head: 'Customer' - name: 'customer' - icon: 'person' - actions: [ + return if _.isEmpty(@params.customer_id) + @item = { + name: 'customer' + badgeCallback: @badgeRender + sidebarHead: 'Customer' + sidebarCallback: @showCustomer + sidebarActions: [ { title: 'Edit Customer' name: 'customer-edit' callback: @editCustomer }, ] - callback: @showCustomer } + metaBadge: (user) => + counter = '' + cssClass = '' + counter = @sidebarItemCounter(user) + + if @Config.get('ui_sidebar_open_ticket_indicator_colored') is true + if counter == 1 + cssClass = 'tabsSidebar-tab-count--warning' + if counter > 1 + cssClass = 'tabsSidebar-tab-count--danger' + + { + name: 'customer' + icon: 'person' + counterPossible: true + counter: counter + cssClass: cssClass + } + + badgeRender: (el) => + @badgeEl = el + if App.User.exists(@params.customer_id) + user = App.User.find(@params.customer_id) + @badgeRenderLocal(user) + + badgeRenderLocal: (user) => + @badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge(user))) + + sidebarItemCounter: (user) -> + counter = '' + if user && user.preferences && user.preferences.tickets_open + counter = user.preferences.tickets_open + counter + showCustomer: (el) => - @el = el + @elSidebar = el + return if _.isEmpty(@params.customer_id) new App.WidgetUser( - el: @el + el: @elSidebar user_id: @params.customer_id + callback: @badgeRenderLocal ) editCustomer: => @@ -32,7 +68,7 @@ class SidebarCustomer extends App.Controller title: 'Users' object: 'User' objects: 'Users' - container: @el.closest('.content') + container: @elSidebar.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 index db2a9239d..de2bc6b26 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_organization.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_organization.coffee @@ -6,24 +6,25 @@ class SidebarOrganization extends App.Controller customer = App.User.find(@params.customer_id) @organization_id = customer.organization_id return if !@organization_id - { - head: 'Organization' + @item = { name: 'organization' - icon: 'group' - actions: [ + badgeIcon: 'group' + sidebarHead: 'Organization' + sidebarCallback: @showOrganization + sidebarActions: [ { title: 'Edit Organization' name: 'organization-edit' callback: @editOrganization }, ] - callback: @showOrganization } + @item showOrganization: (el) => - @el = el + @elSidebar = el new App.WidgetOrganization( - el: @el + el: @elSidebar organization_id: @organization_id ) @@ -35,7 +36,7 @@ class SidebarOrganization extends App.Controller title: 'Organizations' object: 'Organization' objects: 'Organizations' - container: @el.closest('.content') + container: @elSidebar.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 index bb724bc83..318813dec 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_template.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_template.coffee @@ -1,13 +1,15 @@ class SidebarTemplate extends App.Controller sidebarItem: => return if !@permissionCheck('ticket.agent') - { - head: 'Templates' - name: 'template' - icon: 'templates' - actions: [] - callback: @showTemplates + @item = { + name: 'template' + badgeIcon: 'templates' + badgeCallback: @badgeRender + sidebarHead: 'Templates' + sidebarActions: [] + sidebarCallback: @showTemplates } + @item showTemplates: (el) => @el = el diff --git a/app/assets/javascripts/app/controllers/chat.coffee b/app/assets/javascripts/app/controllers/chat.coffee index 2966e3b4c..7414c4c12 100644 --- a/app/assets/javascripts/app/controllers/chat.coffee +++ b/app/assets/javascripts/app/controllers/chat.coffee @@ -35,7 +35,7 @@ class App.CustomerChat extends App.Controller active_agent_ids: [] @render() - @on 'layout-has-changed', @propagateLayoutChange + @on('layout-has-changed', @propagateLayoutChange) # update navbar on new status @bind('chat_status_agent', (data) => @@ -163,6 +163,12 @@ class App.CustomerChat extends App.Controller @title 'Customer Chat', true @navupdate '#customer_chat' + if params.session_id + callback = (session) => + @addChat(session) + App.ChatSession.full(params.session_id, callback) + @navigate '#customer_chat' + active: (state) => return @shown if state is undefined @shown = state @@ -264,10 +270,11 @@ class App.CustomerChat extends App.Controller addChat: (session) -> return if @chatWindows[session.session_id] - chat = new ChatWindow + chat = new ChatWindow( session: session removeCallback: @removeChat messageCallback: @updateNavMenu + ) @workspace.append chat.el chat.render() @@ -289,7 +296,7 @@ class App.CustomerChat extends App.Controller propagateLayoutChange: (event) => # adjust scroll position on layoutChange for session_id, chat of @chatWindows - chat.trigger 'layout-changed' + chat.trigger('layout-changed') acceptChat: => return if @windowCount() >= @maxChatWindows @@ -324,19 +331,6 @@ class App.CustomerChat extends App.Controller currentPosition: => @$('.main').scrollTop() -class CustomerChatRouter extends App.ControllerPermanent - requiredPermission: 'chat.agent' - constructor: (params) -> - super - - App.TaskManager.execute( - key: 'CustomerChat' - controller: 'CustomerChat' - params: {} - show: true - persistent: true - ) - class ChatWindow extends App.Controller className: 'chat-window' @@ -348,6 +342,9 @@ class ChatWindow extends App.Controller 'click .js-close': 'close' 'click .js-disconnect': 'disconnect' 'click .js-scrollHint': 'onScrollHintClick' + 'click .js-info': 'toggleMeta' + 'click .js-createTicket': 'ticketCreate' + 'submit .js-metaForm': 'sendMetaForm' elements: '.js-customerChatInput': 'input' @@ -355,8 +352,11 @@ class ChatWindow extends App.Controller '.js-close': 'closeButton' '.js-disconnect': 'disconnectButton' '.js-body': 'body' + '.js-meta': 'meta' + '.js-name': 'metaName' '.js-scrollHolder': 'scrollHolder' '.js-scrollHint': 'scrollHint' + '.js-metaForm': 'metaForm' sounds: message: new Audio('assets/sounds/chat_message.mp3') @@ -374,9 +374,11 @@ class ChatWindow extends App.Controller @scrollSnapTolerance = 10 # pixels @chat = App.Chat.find(@session.chat_id) - @name = "#{@chat.displayName()} ##{@session.id}" + @name = @chat.displayName() + if @session && !_.isEmpty(@session.name) + @name = @session.name - @on 'layout-change', @onLayoutChange + @on('layout-change', @onLayoutChange) @bind('chat_session_typing', (data) => return if data.session_id isnt @session.session_id @@ -413,12 +415,45 @@ class ChatWindow extends App.Controller onLayoutChange: => @scrollToBottom() - render: -> - @html App.view('customer_chat/chat_window') - name: @name + toggleMeta: => + if @meta.hasClass('hidden') + @showMeta() + else + @hideMeta() - @el.one 'transitionend', @onTransitionend - @scrollHolder.scroll @detectScrolledtoBottom + hideMeta: => + @body.removeClass('hidden') + @meta.addClass('hidden') + @sendMetaForm() + + showMeta: => + @body.addClass('hidden') + @meta.removeClass('hidden') + + sendMetaForm: (e) => + if e + e.preventDefault() + params = @formParam(@metaForm) + + App.WebSocket.send( + event:'chat_session_update' + data: + session_id: @session.session_id + name: params.name + tags: params.tags + ) + + if !_.isEmpty(params.name) + @metaName.text(params.name) + + render: -> + @html App.view('customer_chat/chat_window')( + name: @name + session: @session + ) + + @el.one('transitionend', @onTransitionend) + @scrollHolder.scroll(@detectScrolledtoBottom) # force repaint @el.prop('offsetHeight') @@ -426,18 +461,24 @@ class ChatWindow extends App.Controller # @addMessage 'Hello. My name is Roger, how can I help you?', 'agent' if @session + + # set chat to offline if state is already closed + activeChat = true + if @session.state is 'closed' + activeChat = false + if @session && @session.preferences && @session.preferences.url - @addNoticeMessage(@session.preferences.url) + @addNoticeMessage(@session.preferences.url, undefined, activeChat) if @session.messages for message in @session.messages if message.created_by_id - @addMessage message.content, 'agent' + @addMessage(message.content, 'agent', false, activeChat) else - @addMessage message.content, 'customer' + @addMessage(message.content, 'customer', false, activeChat) # send init reply - if !@session.messages || _.isEmpty(@session.messages) + if activeChat && _.isEmpty(@session.messages) preferences = @Session.get('preferences') if preferences.chat && preferences.chat.phrase phrases = preferences.chat.phrase[@session.chat_id] @@ -447,20 +488,9 @@ class ChatWindow extends App.Controller @input.html(phrase) @sendMessage(1600) - @$('.js-info').popover( - trigger: 'hover' - html: true - animation: false - delay: 0 - placement: 'bottom' - container: 'body' # place in body do prevent it from animating - title: -> - App.i18n.translateContent('Details') - content: => - App.view('customer_chat/chat_window_info')( - session: @session - ) - ) + # set chat to offline if state is already closed + if !activeChat + @goOffline() # show text module UI new App.WidgetTextModule( @@ -470,6 +500,18 @@ class ChatWindow extends App.Controller config: App.Config.all() ) + configureAttributesOutbound = [ + { name: 'name', display: 'Name', tag: 'input', null: true, }, + { name: 'tags', display: 'Tags', tag: 'tag', null: true, }, + ] + new App.ControllerForm( + el: @$('.js-metaForm') + model: + configure_attributes: configureAttributesOutbound + className: '' + params: @session + ) + focus: => @input.focus() @@ -498,7 +540,8 @@ class ChatWindow extends App.Controller @goOffline() close: => - @el.one 'transitionend', { callback: @release }, @onTransitionend + @sendMetaForm() + @el.one('transitionend', { callback: @release }, @onTransitionend) @el.removeClass('is-open') if @removeCallback @removeCallback(@session.session_id) @@ -577,7 +620,8 @@ class ChatWindow extends App.Controller ) @delay(send, delay) - @addMessage content, 'agent' + @hideMeta() + @addMessage(content, 'agent') @input.html('') updateModified: (state) => @@ -614,18 +658,19 @@ class ChatWindow extends App.Controller @messageCallback(@session.session_id) @unreadMessagesCounter = 0 - addMessage: (message, sender, isNew) => - @maybeAddTimestamp() + addMessage: (message, sender, isNew, useMaybeAddTimestamp = true) => + @maybeAddTimestamp() if useMaybeAddTimestamp @lastAddedType = sender - @body.append App.view('customer_chat/chat_message') + @body.append App.view('customer_chat/chat_message')( message: message sender: sender isNew: isNew timestamp: Date.now() + ) - @scrollToBottom showHint: true + @scrollToBottom(showHint: true) showWritingLoader: => if !@isTyping @@ -667,33 +712,37 @@ class ChatWindow extends App.Controller @lastAddedType = 'timestamp' addTimestamp: (label, time) => - @body.append App.view('customer_chat/chat_timestamp') + @body.append App.view('customer_chat/chat_timestamp')( label: label time: time + ) updateLastTimestamp: (label, time) -> @body .find('.js-timestamp') .last() - .replaceWith App.view('customer_chat/chat_timestamp') + .replaceWith App.view('customer_chat/chat_timestamp')( label: label time: time + ) - addStatusMessage: (message, args) -> - @maybeAddTimestamp() + addStatusMessage: (message, args, useMaybeAddTimestamp = true) -> + @maybeAddTimestamp() if useMaybeAddTimestamp - @body.append App.view('customer_chat/chat_status_message') + @body.append App.view('customer_chat/chat_status_message')( message: message args: args + ) @scrollToBottom() - addNoticeMessage: (message, args) -> - @maybeAddTimestamp() + addNoticeMessage: (message, args, useMaybeAddTimestamp = true) -> + @maybeAddTimestamp() if useMaybeAddTimestamp - @body.append App.view('customer_chat/chat_notice_message') + @body.append App.view('customer_chat/chat_notice_message')( message: message args: args + ) @scrollToBottom() @@ -717,6 +766,37 @@ class ChatWindow extends App.Controller else if showHint @showScrollHint() + ticketCreate: (e) => + e.preventDefault() + + id = Math.floor( Math.random() * 99999 ) + @navigate "#ticket/create/id/#{id}" + + # cleanup params + fqdn = App.Config.get('fqdn') + http_type = App.Config.get('http_type') + url = '' + session = @session + + # in case we do not have a model, create one + if session && !session.uiUrl + session = new App.ChatSession(session) + if session && session.uiUrl + url = session.uiUrl() + + clean_params = + id: id + prefilledParams: + body: "#{http_type}://#{fqdn}/#{url}" + title: 'Chat' + + App.TaskManager.execute( + key: "TicketCreateScreen-#{id}" + controller: 'TicketCreate' + params: clean_params + show: true + ) + class Setting extends App.ControllerModal buttonClose: true buttonCancel: true @@ -784,6 +864,24 @@ class Setting extends App.ControllerModal msg: App.i18n.translateContent(data.message) ) +class CustomerChatRouter extends App.ControllerPermanent + requiredPermission: 'chat.agent' + constructor: (params) -> + super + + # cleanup params + clean_params = + session_id: params.session_id + + App.TaskManager.execute( + key: 'CustomerChat' + controller: 'CustomerChat' + params: clean_params + show: true + persistent: true + ) + App.Config.set('customer_chat', CustomerChatRouter, 'Routes') +App.Config.set('customer_chat/session/:session_id', CustomerChatRouter, 'Routes') App.Config.set('CustomerChat', { controller: 'CustomerChat', permission: ['chat.agent'] }, 'permanentTask') App.Config.set('CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', shown: false, permission: ['chat.agent'], class: 'chat' }, 'NavBar') diff --git a/app/assets/javascripts/app/controllers/getting_started.coffee b/app/assets/javascripts/app/controllers/getting_started.coffee index 8c2fbb71d..de73f124e 100644 --- a/app/assets/javascripts/app/controllers/getting_started.coffee +++ b/app/assets/javascripts/app/controllers/getting_started.coffee @@ -337,11 +337,9 @@ class Base extends App.WizardFullScreen @hideAlerts() @disable(e) - # get params @params = @formParam(e.target) - - # add logo @params.logo = @logoPreview.attr('src') + @params.locale_default = App.i18n.detectBrowserLocale() store = (logoResizeDataUrl) => @params.logo_resize = logoResizeDataUrl @@ -354,7 +352,7 @@ class Base extends App.WizardFullScreen success: (data, status, xhr) => if data.result is 'ok' for key, value of data.settings - App.Config.set( key, value ) + App.Config.set(key, value) if App.Config.get('system_online_service') @navigate 'getting_started/channel/email_pre_configured' else diff --git a/app/assets/javascripts/app/controllers/idoit_object_selector.coffee b/app/assets/javascripts/app/controllers/idoit_object_selector.coffee index d2b7f2065..2a7053e50 100644 --- a/app/assets/javascripts/app/controllers/idoit_object_selector.coffee +++ b/app/assets/javascripts/app/controllers/idoit_object_selector.coffee @@ -3,6 +3,7 @@ class App.IdoitObjectSelector extends App.ControllerModal buttonCancel: true buttonSubmit: true head: 'i-doit' + lastSearchTermEmpty: false content: -> @ajax( @@ -44,16 +45,24 @@ class App.IdoitObjectSelector extends App.ControllerModal '' search: (filter) => + if _.isEmpty(filter.type) && _.isEmpty(filter.title) + @lastSearchTermEmpty = true + @renderResult() + return + if _.isEmpty(filter.type) + delete filter.type if _.isEmpty(filter.title) delete filter.title else filter.title = "%#{filter.title}%" + @lastSearchTermEmpty = false @ajax( id: 'idoit-object-selector' type: 'POST' url: "#{@apiPath}/integration/idoit" data: JSON.stringify(method: 'cmdb.objects', filter: filter) success: (data, status, xhr) => + return if @lastSearchTermEmpty @renderResult(data.response.result) error: (xhr, status, error) => diff --git a/app/assets/javascripts/app/controllers/import_zendesk.coffee b/app/assets/javascripts/app/controllers/import_zendesk.coffee index 1858adde1..c49004230 100644 --- a/app/assets/javascripts/app/controllers/import_zendesk.coffee +++ b/app/assets/javascripts/app/controllers/import_zendesk.coffee @@ -154,35 +154,35 @@ class Index extends App.ControllerContent processData: true success: (data, status, xhr) => - if data.result is 'import_done' - window.location.reload() - return - - if data.result is 'error' - @$('.js-error').removeClass('hide') - @$('.js-error').html(App.i18n.translateContent(data.message)) - else - @$('.js-error').addClass('hide') - - if data.message is 'not running' && @updateMigrationDisplayLoop > 16 + if _.isEmpty(data.result) && @updateMigrationDisplayLoop > 16 @$('.js-error').removeClass('hide') @$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.')) return - if data.result is 'in_progress' - for key, item of data.data - if item.done > item.total - item.done = item.total + if !_.isEmpty(data.result['error']) + @$('.js-error').removeClass('hide') + @$('.js-error').html(App.i18n.translateContent(data.result['error'])) + else + @$('.js-error').addClass('hide') - if key == 'Ticket' && item.total >= 1000 + if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error']) + window.location.reload() + return + + if !_.isEmpty(data.result) + for model, stats of data.result + if stats.sum > stats.total + stats.sum = stats.total + + if model == 'Ticket' && stats.total >= 1000 @ticketCountInfo.removeClass('hide') - element = @$('.js-' + key.toLowerCase() ) - element.find('.js-done').text(item.done) - element.find('.js-total').text(item.total) - element.find('progress').attr('max', item.total ) - element.find('progress').attr('value', item.done ) - if item.total <= item.done + element = @$('.js-' + model.toLowerCase() ) + element.find('.js-done').text(stats.sum) + element.find('.js-total').text(stats.total) + element.find('progress').attr('max', stats.total ) + element.find('progress').attr('value', stats.sum ) + if stats.total <= stats.sum element.addClass('is-done') else element.removeClass('is-done') diff --git a/app/assets/javascripts/app/controllers/layout_ref.coffee b/app/assets/javascripts/app/controllers/layout_ref.coffee index f4dc268e7..07ee4bafa 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.coffee @@ -349,7 +349,7 @@ class LayoutRefCommunicationReply extends App.ControllerContent file = @uploadQueue.shift() # console.log "working of", file, "from", @uploadQueue - @fakeUpload file.name, file.size, @workOfUploadQueue + @fakeUpload(file.name, file.size, @workOfUploadQueue) humanFileSize: (size) -> i = Math.floor( Math.log(size) / Math.log(1024) ) @@ -363,27 +363,27 @@ class LayoutRefCommunicationReply extends App.ControllerContent @attachmentPlaceholder.removeClass('hide') @attachmentUpload.addClass('hide') - fakeUpload: (fileName, fileSize, callback) -> + fakeUpload: (filename, size, callback) -> @attachmentPlaceholder.addClass('hide') @attachmentUpload.removeClass('hide') progress = 0 - duration = fileSize / 1024 + duration = size / 1024 for i in [0..100] setTimeout @updateUploadProgress, i*duration/100 , i setTimeout (=> callback() - @renderAttachment(fileName, fileSize) + @renderAttachment(filename, size) ), duration - renderAttachment: (fileName, fileSize) => - @attachments.push([fileName, fileSize]) - @attachmentsHolder.append App.view('generic/attachment_item') - fileName: fileName - fileSize: @humanFileSize(fileSize) - + renderAttachment: (filename, size) => + @attachments.push([filename, size]) + @attachmentsHolder.append(App.view('generic/attachment_item') + filename: filename + size: @humanFileSize(size) + ) App.Config.set( 'layout_ref/communication_reply/:content', LayoutRefCommunicationReply, 'Routes' ) @@ -2121,7 +2121,7 @@ class TwitterConversationRef extends App.ControllerContent open: 88 closed: 20 - maxTextLength: 140 + maxTextLength: 280 warningTextLength: 10 constructor: -> diff --git a/app/assets/javascripts/app/controllers/overview.coffee b/app/assets/javascripts/app/controllers/overview.coffee index 3a8fae430..377328750 100644 --- a/app/assets/javascripts/app/controllers/overview.coffee +++ b/app/assets/javascripts/app/controllers/overview.coffee @@ -23,17 +23,22 @@ class Index extends App.ControllerSubContent ] container: @el.closest('.content') large: true - dndCallback: => + dndCallback: (e, item) => items = @el.find('table > tbody > tr') - order = [] + prios = [] prio = 0 for item in items prio += 1 id = $(item).data('id') - overview = App.Overview.find(id) - if overview.prio isnt prio - overview.prio = prio - overview.save() + prios.push [id, prio] + + @ajax( + id: 'overview_prio' + type: 'POST' + url: "#{@apiPath}/overviews_prio" + processData: true + data: JSON.stringify(prios: prios) + ) ) App.Config.set('Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, permission: ['admin.overview'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/report.coffee b/app/assets/javascripts/app/controllers/report.coffee index 90c3c9d1f..c8cc04620 100644 --- a/app/assets/javascripts/app/controllers/report.coffee +++ b/app/assets/javascripts/app/controllers/report.coffee @@ -108,33 +108,40 @@ class Graph extends App.ControllerContent @render() - render: => + update: (data) => - update = (data) => + # show only selected lines + dataNew = {} + for key, value of data.data + if @params.backendSelected[key] is true + dataNew[key] = value + @ui.storeParams() - # show only selected lines - dataNew = {} - for key, value of data.data - if @params.backendSelected[key] is true - dataNew[key] = value - @ui.storeParams() + if !@lastNewData + @lastNewData = {} - if !@lastNewData - @lastNewData = {} + return if @lastNewData && JSON.stringify(dataNew) is JSON.stringify(@lastNewData) + @lastNewData = dataNew - return if @lastNewData && JSON.stringify(dataNew) is JSON.stringify(@lastNewData) - @lastNewData = dataNew - - @draw(dataNew) - t = new Date - @el.find('#download-chart').html(t.toString()) - new Download( + @draw(dataNew) + t = new Date + @el.find('#download-chart').html(t.toString()) + if @downloadWidget + @downloadWidget.update( + config: @config + params: @params + ui: @ui + ) + else + @downloadWidget = new Download( el: @el.find('.js-dataDownload') config: @config params: @params ui: @ui ) + render: => + url = "#{@apiPath}/reports/generate" interval = 5 * 60000 if @params.timeRange is 'year' @@ -142,9 +149,9 @@ class Graph extends App.ControllerContent if @params.timeRange is 'month' interval = 60000 if @params.timeRange is 'week' - interval = 40000 + interval = 50000 if @params.timeRange is 'day' - interval = 20000 + interval = 30000 if @params.timeRange is 'realtime' interval = 10000 @@ -164,7 +171,7 @@ class Graph extends App.ControllerContent ) processData: true success: (data) => - update(data) + @update(data) @delay(@render, interval, 'report-update', 'page') ) @@ -215,7 +222,7 @@ class Graph extends App.ControllerContent class Download extends App.Controller events: - 'click .js-dataDownloadBackendSelector': 'tableUpdate' + 'click .js-dataDownloadBackendSelector': 'selectBackend' constructor: (data) -> @@ -225,7 +232,24 @@ class Download extends App.Controller super @render() - render: -> + selectBackend: (e) => + e.preventDefault() + @el.find('.js-dataDownloadBackendSelector').parent().removeClass('active') + $(e.target).parent().addClass('active') + @profileSelectedId = $(e.target).data('profile-id') + @params.downloadBackendSelected = $(e.target).data('backend') + @ui.storeParams() + @table = false + @render() + + update: => + @render() + + render: => + + if !@contentRendered + @contentRendered = true + @html(App.view('report/download_content')()) reports = [] @@ -244,44 +268,84 @@ class Download extends App.Controller @profileSelectedId = key profiles.push App.ReportProfile.find(key) - @html App.view('report/download_header')( + downloadHeaderHtml = App.view('report/download_header')( reports: reports profiles: profiles downloadBackendSelected: @params.downloadBackendSelected metric: @config.metric[@params.metric] ) + if downloadHeaderHtml isnt @downloadHeaderHtml + @el.find('.js-dataDownloadHeader').html(downloadHeaderHtml) + @downloadHeaderHtml = downloadHeaderHtml @tableUpdate() - tableUpdate: (e) => - if e - e.preventDefault() - @el.find('.js-dataDownloadBackendSelector').parent().removeClass('active') - $(e.target).parent().addClass('active') - @profileSelectedId = $(e.target).data('profile-id') - @params.downloadBackendSelected = $(e.target).data('backend') - @ui.storeParams() + tableRender: (tickets, count) => + if _.isEmpty(tickets) + @$('.js-dataDownloadButton').html('') + @$('.js-dataDownloadTable').html('') + return - table = (tickets, count) => - url = '#ticket/zoom/' - if App.Config.get('import_mode') - url = App.Config.get('import_otrs_endpoint') + '/index.pl?Action=AgentTicketZoom;TicketID=' - if _.isEmpty(tickets) - @el.find('.js-dataDownloadTable').html('') - else - profile_id = 0 - for key, value of @params.profileSelected - if value - profile_id = key - downloadUrl = "#{@apiPath}/reports/sets?sheet=true;metric=#{@params.metric};year=#{@params.year};month=#{@params.month};week=#{@params.week};day=#{@params.day};timeRange=#{@params.timeRange};profile_id=#{profile_id};downloadBackendSelected=#{@params.downloadBackendSelected}" - html = App.view('report/download_list')( - tickets: tickets - count: count - url: url - download: downloadUrl - ) - @el.find('.js-dataDownloadTable').html(html) + profile_id = 0 + for key, value of @params.profileSelected + if value + profile_id = key + downloadUrl = "#{@apiPath}/reports/sets?sheet=true;metric=#{@params.metric};year=#{@params.year};month=#{@params.month};week=#{@params.week};day=#{@params.day};timeRange=#{@params.timeRange};profile_id=#{profile_id};downloadBackendSelected=#{@params.downloadBackendSelected}" + @$('.js-dataDownloadButton').html(App.view('report/download_button')( + count: count + downloadUrl: downloadUrl + )) + openTicket = (id,e) => + ticket = App.Ticket.findNative(id) + @navigate ticket.uiUrl() + callbackTicketTitleAdd = (value, object, attribute, attributes) -> + attribute.title = object.title + value + callbackLinkToTicket = (value, object, attribute, attributes) -> + attribute.link = object.uiUrl() + value + callbackIconHeader = (headers) -> + attribute = + name: 'icon' + display: '' + translation: false + width: '28px' + displayWidth:28 + unresizable: true + headers.unshift(0) + headers[0] = attribute + headers + callbackIcon = (value, object, attribute, header) -> + value = ' ' + attribute.class = object.iconClass() + attribute.link = '' + attribute.title = object.iconTitle() + value + + params = + el: @el.find('.js-dataDownloadTable') + model: App.Ticket + objects: tickets + overviewAttributes: ['number', 'title', 'state', 'group', 'created_at'] + bindRow: + events: + 'click': openTicket + callbackHeader: [ callbackIconHeader ] + callbackAttributes: + icon: + [ callbackIcon ] + title: + [ callbackLinkToTicket, callbackTicketTitleAdd ] + number: + [ callbackLinkToTicket, callbackTicketTitleAdd ] + + if !@table + @table = new App.ControllerTable(params) + else + @table.update(objects: tickets) + + tableUpdate: => @ajax( id: 'report_download' type: 'POST' @@ -298,15 +362,14 @@ class Download extends App.Controller downloadBackendSelected: @params.downloadBackendSelected ) processData: true - success: (data) -> + success: (data) => App.Collection.loadAssets(data.assets) ticket_collection = [] if data.ticket_ids for record_id in data.ticket_ids - ticket = App.Ticket.fullLocal( record_id ) + ticket = App.Ticket.fullLocal(record_id) ticket_collection.push ticket - - table(ticket_collection, data.count) + @tableRender(ticket_collection, data.count) ) class TimeRangePicker extends App.Controller diff --git a/app/assets/javascripts/app/controllers/search.coffee b/app/assets/javascripts/app/controllers/search.coffee index 9a6f45052..67e9c009a 100644 --- a/app/assets/javascripts/app/controllers/search.coffee +++ b/app/assets/javascripts/app/controllers/search.coffee @@ -79,6 +79,7 @@ class App.Search extends App.Controller @tabs = [] for model in App.Config.get('models_searchable') + model = model.replace(/::/, '') tab = name: model model: model diff --git a/app/assets/javascripts/app/controllers/ticket_customer.coffee b/app/assets/javascripts/app/controllers/ticket_customer.coffee index edb5df0c0..713761a10 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, disableCreateObject: true }, + { name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: false }, ] controller = new App.ControllerForm( model: @@ -18,8 +18,19 @@ class App.TicketCustomer extends App.ControllerModal onSubmit: (e) => params = @formParam(e.target) - @customer_id = params['customer_id'] + ticket = App.Ticket.find(@ticket_id) + ticket.customer_id = params['customer_id'] + errors = ticket.validate() + if !_.isEmpty(errors) + @log 'error', errors + @formValidate( + form: e.target + errors: errors + ) + return + + @customer_id = params['customer_id'] callback = => # close modal diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index de9d4df17..40f9b7a7e 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -956,7 +956,6 @@ class Table extends App.Controller ticketListShow = [] for ticket in tickets ticketListShow.push App.Ticket.find(ticket.id) - console.log('overview', overview) @overview = App.Overview.find(overview.id) @table.update( overviewAttributes: @overview.view.s diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index 094a15f30..d05db0448 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -461,6 +461,7 @@ class App.TicketZoom extends App.Controller ui: @ highligher: @highligher ticket_article_ids: @ticket_article_ids + form_id: @form_id ) new App.TicketCustomerAvatar( diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee new file mode 100644 index 000000000..7436c15f6 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/delete.coffee @@ -0,0 +1,34 @@ +class Delete + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + if article.type.name is 'note' + user = undefined + if App.Session.get('id') == article.created_by_id + user = App.User.find(App.Session.get('id')) + if user.permission('ticket.agent') + actions.push { + name: 'delete' + type: 'delete' + icon: 'trash' + href: '#' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'delete' + + callback = -> + article = App.TicketArticle.find(article.id) + article.destroy() + + new App.ControllerConfirm( + message: 'Sure?' + callback: callback + container: ui.el.closest('.content') + ) + + true + +App.Config.set('900-Delete', Delete, 'TicketZoomArticleAction') diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee new file mode 100644 index 000000000..f233d2bb2 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_action/email_reply.coffee @@ -0,0 +1,203 @@ +class EmailReply extends App.Controller + @action: (actions, ticket, article, ui) -> + return actions if ui.permissionCheck('ticket.customer') + + group = ticket.group + if group.email_address_id && (article.type.name is 'email' || article.type.name is 'web') + actions.push { + name: 'reply' + type: 'emailReply' + icon: 'reply' + href: '#' + } + recipients = [] + if article.sender.name is 'Customer' + if article.from + localRecipients = emailAddresses.parseAddressList(article.from) + if localRecipients + recipients = recipients.concat localRecipients + if article.to + localRecipients = emailAddresses.parseAddressList(article.to) + if localRecipients + recipients = recipients.concat localRecipients + if article.cc + localRecipients = emailAddresses.parseAddressList(article.cc) + if localRecipients + recipients = recipients.concat localRecipients + + # remove system addresses + localAddresses = App.EmailAddress.all() + forgeinRecipients = [] + recipientUsed = {} + for recipient in recipients + if !_.isEmpty(recipient.address) + localRecipientAddress = recipient.address.toString().toLowerCase() + if !recipientUsed[localRecipientAddress] + recipientUsed[localRecipientAddress] = true + localAddress = false + for address in localAddresses + if localRecipientAddress is address.email.toString().toLowerCase() + recipientUsed[localRecipientAddress] = true + localAddress = true + if !localAddress + forgeinRecipients.push recipient + + # check if reply all is neede + if forgeinRecipients.length > 1 + actions.push { + name: 'reply all' + type: 'emailReplyAll' + icon: 'reply-all' + href: '#' + } + + actions.push { + name: 'forward' + type: 'emailForward' + icon: 'forward' + href: '#' + } + + if article.sender.name is 'Customer' && article.type.name is 'phone' + actions.push { + name: 'reply' + type: 'emailReply' + icon: 'reply' + href: '#' + } + actions.push { + name: 'forward' + type: 'emailForward' + icon: 'forward' + href: '#' + } + if article.sender.name is 'Agent' && article.type.name is 'phone' + actions.push { + name: 'reply' + type: 'emailReply' + icon: 'reply' + href: '#' + } + actions.push { + name: 'forward' + type: 'emailForward' + icon: 'forward' + href: '#' + } + + actions + + @perform: (articleContainer, type, ticket, article, ui) -> + return true if type isnt 'emailReply' && type isnt 'emailReplyAll' && type isnt 'emailForward' + + if type is 'emailReply' + @emailReply(false, ticket, article, ui) + + else if type is 'emailReplyAll' + @emailReply(true, ticket, article, ui) + + else if type is 'emailForward' + @emailForward(ticket, article, ui) + + true + + @emailReply: (all = false, ticket, article, ui) -> + + # get reference article + type = App.TicketArticleType.find(article.type_id) + article_created_by = App.User.find(article.created_by_id) + email_addresses = App.EmailAddress.all() + + ui.scrollToCompose() + + # empty form + articleNew = App.Utils.getRecipientArticle(ticket, article, article_created_by, type, email_addresses, all) + + # get current body + body = ui.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() + if !selected + selected = App.ClipBoard.getSelected('text') + 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}
#{body}
#{selected}