diff --git a/.coffeelint/rules/prevent_underscore_backport.coffee b/.coffeelint/rules/prevent_underscore_backport.coffee new file mode 100644 index 000000000..d87fc2b79 --- /dev/null +++ b/.coffeelint/rules/prevent_underscore_backport.coffee @@ -0,0 +1,20 @@ +module.exports = class PreventUnderscoreBackport + + rule: + name: 'prevent_underscore_backport' + level: 'error' + message: 'The method __(...) is not available in current stable' + description: ''' + ''' + + constructor: -> + @callTokens = [] + + tokens: ['CALL_START'] + + lintToken: (token, tokenApi) -> + [type, tokenValue] = token + + p = tokenApi.peek(-1) + if p[1] == '__' + return { } diff --git a/.gitignore b/.gitignore index 2b039d19c..635e6d5e5 100644 --- a/.gitignore +++ b/.gitignore @@ -84,6 +84,9 @@ # Eclipse /.project +# VSCode +/.vscode + # Byebug /.byebug_history diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ad6f8a5f..3a4db5d1a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ default: - image: registry.znuny.com/docker/zammad-ci:2.7.4 + image: $CI_REGISTRY/docker/zammad-ci:2.7.4 include: - local: '/.gitlab/ci/base.yml' @@ -55,7 +55,7 @@ cache: # Initialize application env before_script: - source /etc/profile.d/rvm.sh - - source /opt/rh/rh-nodejs12/enable + - source /opt/rh/rh-nodejs*/enable - bundle install -j $(nproc) --path vendor - bundle exec ruby .gitlab/configure_environment.rb - source .gitlab/environment.env diff --git a/.gitlab/ci/base.yml b/.gitlab/ci/base.yml index 9f5c65042..2c999e26c 100644 --- a/.gitlab/ci/base.yml +++ b/.gitlab/ci/base.yml @@ -74,35 +74,35 @@ # DB Docker .docker_mysql: &docker_mysql - name: registry.znuny.com/docker/zammad-mysql:stable + name: $CI_REGISTRY/docker/zammad-mysql:stable alias: mysql .docker_postgresql: &docker_postgresql - name: registry.znuny.com/docker/zammad-postgresql:stable + name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql .docker_elasticsearch: &docker_elasticsearch - name: registry.znuny.com/docker/zammad-elasticsearch:$ELASTICSEARCH_TAG + name: $CI_REGISTRY/docker/zammad-elasticsearch:$ELASTICSEARCH_TAG alias: elasticsearch .docker_selenium_chrome: &docker_selenium_chrome - name: registry.znuny.com/docker/zammad-selenium-chrome:stable + name: $CI_REGISTRY/docker/zammad-selenium-chrome:stable alias: selenium-chrome .docker_selenium_firefox: &docker_selenium_firefox - name: registry.znuny.com/docker/zammad-selenium-firefox:stable + name: $CI_REGISTRY/docker/zammad-selenium-firefox:stable alias: selenium-firefox .docker_imap: &docker_imap - name: registry.znuny.com/docker/zammad-imap:stable + name: $CI_REGISTRY/docker/zammad-imap:stable alias: mail .docker_redis: &docker_redis - name: registry.znuny.com/docker/zammad-redis:stable + name: $CI_REGISTRY/docker/zammad-redis:stable alias: redis .docker_memcached: &docker_memcached - name: registry.znuny.com/docker/zammad-memcached:stable + name: $CI_REGISTRY/docker/zammad-memcached:stable alias: memcached command: ["memcached", "-m", "256M"] diff --git a/.gitlab/ci/browser-core/api_client_php.yml b/.gitlab/ci/browser-core/api_client_php.yml index 7ded57e08..1965c2297 100644 --- a/.gitlab/ci/browser-core/api_client_php.yml +++ b/.gitlab/ci/browser-core/api_client_php.yml @@ -8,7 +8,7 @@ api_client_php: script: - RAILS_ENV=test bundle exec rake db:create - RAILS_ENV=test bundle exec rake zammad:ci:test:start zammad:setup:auto_wizard - - git clone https://github.com/zammad/zammad-api-client-php.git + - git clone https://github.com/zammad/zammad-api-client-php.git -b zammad-ci-5.0 # Use state with tests compatible to 5.0 - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - php composer-setup.php --install-dir=/usr/local/bin - ln -s /usr/local/bin/composer.phar /usr/local/bin/composer diff --git a/.gitlab/ci/browser-integration/otrs_chrome.yml b/.gitlab/ci/browser-integration/otrs_chrome.yml index 6f96953aa..79ae8ed9a 100644 --- a/.gitlab/ci/browser-integration/otrs_chrome.yml +++ b/.gitlab/ci/browser-integration/otrs_chrome.yml @@ -7,11 +7,11 @@ otrs_chrome: IMPORT_OTRS_ENDPOINT: "http://zammad-ci-otrsimport-app/otrs/public.pl?Action=ZammadMigrator" TZ: "Europe/Berlin" # Required for the zammad-ci-otrsimport-app containers services: - - name: registry.znuny.com/docker/zammad-postgresql:stable + - name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql - - name: registry.znuny.com/docker/zammad-selenium-chrome:stable + - name: $CI_REGISTRY/docker/zammad-selenium-chrome:stable alias: selenium-chrome - - name: registry.znuny.com/docker/zammad-ci-otrsimport-db:otrs6 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-db:otrs6 alias: zammad-ci-otrsimport-db - - name: registry.znuny.com/docker/zammad-ci-otrsimport-app:otrs6 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs6 alias: zammad-ci-otrsimport-app diff --git a/.gitlab/ci/integration/es.yml b/.gitlab/ci/integration/es.yml index 79464bddf..15039a6c7 100644 --- a/.gitlab/ci/integration/es.yml +++ b/.gitlab/ci/integration/es.yml @@ -11,7 +11,6 @@ - bundle exec rails test test/integration/elasticsearch_active_test.rb - bundle exec rails test test/integration/elasticsearch_test.rb - bundle exec rspec --tag searchindex --tag ~type:system --profile 10 - - bundle exec rails test test/integration/report_test.rb es:7: <<: *template_integration_es diff --git a/.gitlab/ci/integration/otrs.yml b/.gitlab/ci/integration/otrs.yml index 2eddc231e..040520306 100644 --- a/.gitlab/ci/integration/otrs.yml +++ b/.gitlab/ci/integration/otrs.yml @@ -12,71 +12,71 @@ otrs:6: <<: *template_integration_otrs services: - - name: registry.znuny.com/docker/zammad-mysql:stable + - name: $CI_REGISTRY/docker/zammad-mysql:stable alias: mysql - - name: registry.znuny.com/docker/zammad-postgresql:stable + - name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql - - name: registry.znuny.com/docker/zammad-ci-otrsimport-db:otrs6 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-db:otrs6 alias: zammad-ci-otrsimport-db - - name: registry.znuny.com/docker/zammad-ci-otrsimport-app:otrs6 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs6 alias: zammad-ci-otrsimport-app otrs:5: <<: *template_integration_otrs services: - - name: registry.znuny.com/docker/zammad-mysql:stable + - name: $CI_REGISTRY/docker/zammad-mysql:stable alias: mysql - - name: registry.znuny.com/docker/zammad-postgresql:stable + - name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql - - name: registry.znuny.com/docker/zammad-ci-otrsimport-db:otrs5 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-db:otrs5 alias: zammad-ci-otrsimport-db - - name: registry.znuny.com/docker/zammad-ci-otrsimport-app:otrs5 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs5 alias: zammad-ci-otrsimport-app otrs:4: <<: *template_integration_otrs services: - - name: registry.znuny.com/docker/zammad-mysql:stable + - name: $CI_REGISTRY/docker/zammad-mysql:stable alias: mysql - - name: registry.znuny.com/docker/zammad-postgresql:stable + - name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql - - name: registry.znuny.com/docker/zammad-ci-otrsimport-db:otrs4 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-db:otrs4 alias: zammad-ci-otrsimport-db - - name: registry.znuny.com/docker/zammad-ci-otrsimport-app:otrs4 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs4 alias: zammad-ci-otrsimport-app otrs:33: <<: *template_integration_otrs services: - - name: registry.znuny.com/docker/zammad-mysql:stable + - name: $CI_REGISTRY/docker/zammad-mysql:stable alias: mysql - - name: registry.znuny.com/docker/zammad-postgresql:stable + - name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql - - name: registry.znuny.com/docker/zammad-ci-otrsimport-db:otrs33 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-db:otrs33 alias: zammad-ci-otrsimport-db - - name: registry.znuny.com/docker/zammad-ci-otrsimport-app:otrs33 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs33 alias: zammad-ci-otrsimport-app otrs:32: <<: *template_integration_otrs services: - - name: registry.znuny.com/docker/zammad-mysql:stable + - name: $CI_REGISTRY/docker/zammad-mysql:stable alias: mysql - - name: registry.znuny.com/docker/zammad-postgresql:stable + - name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql - - name: registry.znuny.com/docker/zammad-ci-otrsimport-db:otrs32 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-db:otrs32 alias: zammad-ci-otrsimport-db - - name: registry.znuny.com/docker/zammad-ci-otrsimport-app:otrs32 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs32 alias: zammad-ci-otrsimport-app otrs:31: <<: *template_integration_otrs services: - - name: registry.znuny.com/docker/zammad-mysql:stable + - name: $CI_REGISTRY/docker/zammad-mysql:stable alias: mysql - - name: registry.znuny.com/docker/zammad-postgresql:stable + - name: $CI_REGISTRY/docker/zammad-postgresql:stable alias: postgresql - - name: registry.znuny.com/docker/zammad-ci-otrsimport-db:otrs31 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-db:otrs31 alias: zammad-ci-otrsimport-db - - name: registry.znuny.com/docker/zammad-ci-otrsimport-app:otrs31 + - name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs31 alias: zammad-ci-otrsimport-app diff --git a/.gitlab/ci/pre.yml b/.gitlab/ci/pre.yml index 5a6d2a651..8985901e7 100644 --- a/.gitlab/ci/pre.yml +++ b/.gitlab/ci/pre.yml @@ -6,7 +6,7 @@ - .rules_singletest before_script: - source /etc/profile.d/rvm.sh # ensure RVM is loaded - - source /opt/rh/rh-nodejs12/enable # ensure Node.js is available + - source /opt/rh/rh-nodejs*/enable # ensure Node.js is available rubocop: <<: *template_pre @@ -39,7 +39,7 @@ brakeman: artifacts: expire_in: 1 week paths: - - tmp/brakeman-report.html + - tmp/brakeman-report.html when: on_failure script: - bundle install -j $(nproc) --path vendor @@ -48,7 +48,7 @@ brakeman: coffeelint: <<: *template_pre script: - - coffeelint app/ + - coffeelint --rules ./.coffeelint/rules/* app/ bundle-audit: <<: *template_pre @@ -62,8 +62,9 @@ github: tags: - deploy before_script: - - "" # no RVM present in deploy ENV + - '' # no RVM present in deploy ENV script: + - git fetch --unshallow - script/build/sync_repo.sh git@github.com:zammad/zammad.git global_refresh_envs: @@ -77,7 +78,7 @@ global_refresh_envs: artifacts: expire_in: 1 day paths: - - fresh.env + - fresh.env rules: - if: $CI_MERGE_REQUEST_ID when: never diff --git a/.pkgr.yml b/.pkgr.yml index e4468d5b5..b6995250b 100644 --- a/.pkgr.yml +++ b/.pkgr.yml @@ -56,6 +56,18 @@ targets: - libimlib2 - libimlib2-dev - shared-mime-info + debian-11: + dependencies: + - curl + - elasticsearch|elasticsearch-oss + - nginx|apache2 + - postgresql|mariadb-server + - libimlib2 + - shared-mime-info + build_dependencies: + - libimlib2 + - libimlib2-dev + - shared-mime-info ubuntu-16.04: dependencies: - curl diff --git a/.rubocop/cop/zammad/exists_db_strategy.rb b/.rubocop/cop/zammad/exists_db_strategy.rb index f825e2bfc..33fd0de2f 100644 --- a/.rubocop/cop/zammad/exists_db_strategy.rb +++ b/.rubocop/cop/zammad/exists_db_strategy.rb @@ -17,7 +17,7 @@ module RuboCop PATTERN def_node_matcher :has_reset?, <<-PATTERN - $(send _ {:describe :context :it} (_ ...) (hash ... (pair (sym :db_strategy) (sym {:reset :reset_all})))) + $(send _ {:describe :context :it :shared_examples} (_ ...) (hash <(pair (sym :db_strategy) (sym {:reset :reset_all})) ...> )) PATTERN MSG = 'Add a `db_strategy: :reset` to your context/decribe when you are creating object manager attributes!'.freeze diff --git a/.rubocop/cop/zammad/prevent_underscore_backport.rb b/.rubocop/cop/zammad/prevent_underscore_backport.rb new file mode 100644 index 000000000..7bb6791a3 --- /dev/null +++ b/.rubocop/cop/zammad/prevent_underscore_backport.rb @@ -0,0 +1,17 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +module RuboCop + module Cop + module Zammad + class PreventUnderscroreBackport < Base + MSG = <<~ERROR_MESSAGE.freeze + The method __(...) is not available in current stable. + ERROR_MESSAGE + + def on_send(node) + add_offense(node) if node.method_name.eql? :__ + end + end + end + end +end diff --git a/.rubocop/cop/zammad/update_copyright.rb b/.rubocop/cop/zammad/update_copyright.rb index ffbb3774c..7a7202c3a 100644 --- a/.rubocop/cop/zammad/update_copyright.rb +++ b/.rubocop/cop/zammad/update_copyright.rb @@ -13,7 +13,8 @@ module RuboCop def on_new_investigation if processed_source.raw_source.include? '# Copyright (C) 2012-' - update_copyright + # Disabled for stable branches. + # update_copyright else insert_copyright end diff --git a/.rubocop/rubocop_zammad.rb b/.rubocop/rubocop_zammad.rb index f3e8764cd..04ff86a9c 100644 --- a/.rubocop/rubocop_zammad.rb +++ b/.rubocop/rubocop_zammad.rb @@ -10,3 +10,4 @@ require_relative 'cop/zammad/no_to_sym_on_string' require_relative 'cop/zammad/prefer_negated_if_over_unless' require_relative 'cop/zammad/update_copyright' require_relative 'cop/zammad/forbid_rand' +require_relative 'cop/zammad/prevent_underscore_backport' diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4dfd0dc..7fe6c3c78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,88 @@ # Change Log +## [5.0.3](https://github.com/zammad/zammad/tree/5.0.3) (2021-12-07) + +[Full Changelog](https://github.com/zammad/zammad/compare/5.0.2...5.0.3) + +**Implemented enhancements:** + +- Possibility to specify the order of objects [294](https://github.com/zammad/zammad/issues/294) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)] +- Display callback urls for third-party applications [3622](https://github.com/zammad/zammad/issues/3622) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] +- Add clear selection action or has changed condition [3821](https://github.com/zammad/zammad/issues/3821) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[specification required](https://github.com/zammad/zammad/labels/specification required)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] + +**Fixed bugs:** + +- Missing ticket updates on high load in MariaDB/MySQL environments [3877](https://github.com/zammad/zammad/issues/3877) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Wrong SLA is used (alphabetical order is ignored) [3871](https://github.com/zammad/zammad/issues/3871) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[regression](https://github.com/zammad/zammad/labels/regression)] +- Agent with CREATE only permission acts "on behalf" ticket customer on shared organizations [3872](https://github.com/zammad/zammad/issues/3872) [[bug](https://github.com/zammad/zammad/labels/bug)] +- Provide meaningful modal if report profile tries to use dates out side the filtered date range [3616](https://github.com/zammad/zammad/issues/3616) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[reporting](https://github.com/zammad/zammad/labels/reporting)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Allow `position` to determine an attributes position entirely [3594](https://github.com/zammad/zammad/issues/3594) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)] [[specification required](https://github.com/zammad/zammad/labels/specification required)] +- Till label not assigned to corresponding input fields in calendar edit view [3793](https://github.com/zammad/zammad/issues/3793) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] +- Simple quote characters (`'`) not properly displayed [3846](https://github.com/zammad/zammad/issues/3846) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] +- Remove api user and password for sipgate integration [3848](https://github.com/zammad/zammad/issues/3848) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin area)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- No signature on new ticket if email is default message type [3844](https://github.com/zammad/zammad/issues/3844) [[bug](https://github.com/zammad/zammad/labels/bug)] +- Forwarding no longer possible for email and web articles [3855](https://github.com/zammad/zammad/issues/3855) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[regression](https://github.com/zammad/zammad/labels/regression)] +- Default values of new group dialog is different in german vs. english [3851](https://github.com/zammad/zammad/issues/3851) [[bug](https://github.com/zammad/zammad/labels/bug)] +- If no date is set the UI show it's shown as NaN.NaN.NaN. [3850](https://github.com/zammad/zammad/issues/3850) [[bug](https://github.com/zammad/zammad/labels/bug)] +- Number of to show caller log entries is inconsistent setting wise [3852](https://github.com/zammad/zammad/issues/3852) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[admin area](https://github.com/zammad/zammad/labels/admin area)] [[integration](https://github.com/zammad/zammad/labels/integration)] +- Reply all: Duplicate email on changing recipient and cc in a certain way [3825](https://github.com/zammad/zammad/issues/3825) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Removing calendars via UI and API does not check for references [3845](https://github.com/zammad/zammad/issues/3845) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[escalation logic](https://github.com/zammad/zammad/labels/escalation logic)] +- Email address not shown inside forwarded email [3824](https://github.com/zammad/zammad/issues/3824) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Wrong escalation update time in admin interface is shown [3837](https://github.com/zammad/zammad/issues/3837) [[bug](https://github.com/zammad/zammad/labels/bug)] +- Zammad returns stack error when one tries to remove groups via API [3841](https://github.com/zammad/zammad/issues/3841) [[bug](https://github.com/zammad/zammad/labels/bug)] [[API](https://github.com/zammad/zammad/labels/API)] +- Zammad ignores relative GitLab URLs [3830](https://github.com/zammad/zammad/issues/3830) [[bug](https://github.com/zammad/zammad/labels/bug)] [[integration](https://github.com/zammad/zammad/labels/integration)] +- If selected value is not part of the restriction of set_fixed_to it should recalculate it with the new value [3822](https://github.com/zammad/zammad/issues/3822) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Invalid group and owner list for tickets created via customer profile [3835](https://github.com/zammad/zammad/issues/3835) [[bug](https://github.com/zammad/zammad/labels/bug)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] +- Ticket zoom will loose attachments on rerender [3831](https://github.com/zammad/zammad/issues/3831) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Package installation creates database.yml as `root` and thus breaks the installation until next update [3834](https://github.com/zammad/zammad/issues/3834) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[regression](https://github.com/zammad/zammad/labels/regression)] +- Ticket create screen will loose attachments by time [3827](https://github.com/zammad/zammad/issues/3827) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Simple quote characters (`'`) not properly displayed [3733](https://github.com/zammad/zammad/issues/3733) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] +- When quoting, no breakout from div container possible [3094](https://github.com/zammad/zammad/issues/3094) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] +- Quoting not working cleanly, if content gets too much [2334](https://github.com/zammad/zammad/issues/2334) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Zammad database credentials are world-readable [3828](https://github.com/zammad/zammad/issues/3828) [[bug](https://github.com/zammad/zammad/labels/bug)] +- Update time SLAs escalates tickets with agent response [3140](https://github.com/zammad/zammad/issues/3140) [[bug](https://github.com/zammad/zammad/labels/bug)] [[escalation logic](https://github.com/zammad/zammad/labels/escalation logic)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] + +## [5.0.2](https://github.com/zammad/zammad/tree/5.0.2) (2021-10-28) + +[Full Changelog](https://github.com/zammad/zammad/compare/5.0.1...5.0.2) + +**Fixed bugs:** + +- When looking for customers, it is no longer possible to change into organizations [3815](https://github.com/zammad/zammad/issues/3815) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] +- Owner should get cleared if not listed in changed group [3818](https://github.com/zammad/zammad/issues/3818) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] +- Custom date and datetime attributes are filled with dates on creation of tickets/users after update from 4.1 to 5.x [3810](https://github.com/zammad/zammad/issues/3810) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- ActionController::UnknownHttpMethod FRAGE [3807](https://github.com/zammad/zammad/issues/3807) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[macros](https://github.com/zammad/zammad/labels/macros)] +- Remote change of the group id does show it falsly as user change and not render the new value to the ticket [3801](https://github.com/zammad/zammad/issues/3801) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] +- Adding private keys allows adding certificates [3727](https://github.com/zammad/zammad/issues/3727) [[bug](https://github.com/zammad/zammad/labels/bug)] +- Able to create custom fields for existing relation (e. g. ticket.state) - will lead to non bootable Zammad [3811](https://github.com/zammad/zammad/issues/3811) [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)] +- Sort order group_by broken (alphabetical) [3800](https://github.com/zammad/zammad/issues/3800) [[bug](https://github.com/zammad/zammad/labels/bug)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[regression](https://github.com/zammad/zammad/labels/regression)] +- Ticket owner selection is not updated if owner selection should be empty [3809](https://github.com/zammad/zammad/issues/3809) [[bug](https://github.com/zammad/zammad/labels/bug)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] +- Chat can't be closed after timeout [2471](https://github.com/zammad/zammad/issues/2471) [[bug](https://github.com/zammad/zammad/labels/bug)] [[chat](https://github.com/zammad/zammad/labels/chat)] +- Support workflow mechanism to do pending reminder state hide pending time use case [3790](https://github.com/zammad/zammad/issues/3790) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] +- Cache.clear in postinstall.sh throws ugly errors on fresh installations [3808](https://github.com/zammad/zammad/issues/3808) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[regression](https://github.com/zammad/zammad/labels/regression)] +- Example payload in webhook view leads to 500 error [3794](https://github.com/zammad/zammad/issues/3794) [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)] +- OS package upgrade fails (activity_stream_object_id) [3797](https://github.com/zammad/zammad/issues/3797) [[bug](https://github.com/zammad/zammad/labels/bug)] + ## [5.0.1](https://github.com/zammad/zammad/tree/5.0.1) (2021-10-08) + [Full Changelog](https://github.com/zammad/zammad/compare/5.0.0...5.0.1) **Fixed bugs:** -- Bug Report 4.1.x Overview Sort - Grouped by user [3737](https://github.com/zammad/zammad/issues/3737) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[regression](https://github.com/zammad/zammad/labels/regression)] + +- Bug Report 4.1.x Overview Sort - Grouped by user [3737](https://github.com/zammad/zammad/issues/3737) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[regression](https://github.com/zammad/zammad/labels/regression)] - Article box opening on tickets with no changes [3789](https://github.com/zammad/zammad/issues/3789) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] - UploadCacheCleanupJob does not execute [3787](https://github.com/zammad/zammad/issues/3787) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] - lib/fill_db.rb fails to work in production environments [3788](https://github.com/zammad/zammad/issues/3788) [[bug](https://github.com/zammad/zammad/labels/bug)] - ## [5.0.0](https://github.com/zammad/zammad/tree/5.0.0) (2021-10-05) + [Full Changelog](https://github.com/zammad/zammad/compare/4.1.0...5.0.0) **Implemented enhancements:** + - Core Workflow: Add organization condition attributes for object User [3779](https://github.com/zammad/zammad/issues/3779) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] - No script content (e. g. javascript) in emails [3365](https://github.com/zammad/zammad/issues/3365) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)] -- Read-only custom objects [2102](https://github.com/zammad/zammad/issues/2102) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] +- Read-only custom objects [2102](https://github.com/zammad/zammad/issues/2102) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[object manager attribute](https://github.com/zammad/zammad/labels/object manager attribute)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] - Granular admin permission for google channel is missing [3194](https://github.com/zammad/zammad/issues/3194) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] - Use country codes (e.g. `DE` or `ES`) for knowledgebase answer selection [3574](https://github.com/zammad/zammad/issues/3574) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)] - New email account expert view cannot be opened without filling in all fields [3137](https://github.com/zammad/zammad/issues/3137) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] @@ -34,7 +100,7 @@ - Visualise locked users in UI and make them unlock-able for admin [2565](https://github.com/zammad/zammad/issues/2565) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] - Representation of inactive customers and orgnizations [3302](https://github.com/zammad/zammad/issues/3302) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] - No possibility to enforce auto response if one of the blocking auto response mail header exists [3667](https://github.com/zammad/zammad/issues/3667) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised by payment)] [[mail processing](https://github.com/zammad/zammad/labels/mail processing)] -- REST doc of Online Notification controler is outdated/wrong and expand param is missing. [3635](https://github.com/zammad/zammad/issues/3635) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] +- REST doc of Online Notification controler is outdated/wrong and expand param is missing. [3635](https://github.com/zammad/zammad/issues/3635) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] - Scroll background instead of foreground [978](https://github.com/zammad/zammad/issues/978) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend / JS app)] - Log if a active user (in UI) has been logged out due to SessionTimeout [3614](https://github.com/zammad/zammad/issues/3614) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] - The rake task `zammad:package:migrate` does not execute migrations for linked packages. [3606](https://github.com/zammad/zammad/issues/3606) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[developer experience](https://github.com/zammad/zammad/labels/developer experience)] @@ -44,7 +110,8 @@ - Display minutes for session timeouts instead of seconds [3575](https://github.com/zammad/zammad/issues/3575) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[admin area](https://github.com/zammad/zammad/labels/admin area)] **Fixed bugs:** -- Inconstant alignment in the listing of attachments/submit button in new article area [3773](https://github.com/zammad/zammad/issues/3773) [[bug](https://github.com/zammad/zammad/labels/bug)] + +- Inconstant alignment in the listing of attachments/submit button in new article area [3773](https://github.com/zammad/zammad/issues/3773) [[bug](https://github.com/zammad/zammad/labels/bug)] - Improve contrasts in answer search for articles [3783](https://github.com/zammad/zammad/issues/3783) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)] - escaped 'Set fixed' workflows don't refresh set values on active ticket sessions [3757](https://github.com/zammad/zammad/issues/3757) [[bug](https://github.com/zammad/zammad/labels/bug)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] - ObjectManager Attribute without screen attribute causes CoreWorkflows migration to fail [3781](https://github.com/zammad/zammad/issues/3781) [[bug](https://github.com/zammad/zammad/labels/bug)] [[migration / update](https://github.com/zammad/zammad/labels/migration / update)] @@ -59,7 +126,7 @@ - Possible race condition causing OTRS import to fail [3765](https://github.com/zammad/zammad/issues/3765) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] - Incorrect alignment in the listing of attachments when creating a ticket [3746](https://github.com/zammad/zammad/issues/3746) [[bug](https://github.com/zammad/zammad/labels/bug)] - Saved conditions break on selections without reloading [3758](https://github.com/zammad/zammad/issues/3758) [[bug](https://github.com/zammad/zammad/labels/bug)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] -- Misleading view of user icons which are on vacation and disabled [3075](https://github.com/zammad/zammad/issues/3075) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] +- Misleading view of user icons which are on vacation and disabled [3075](https://github.com/zammad/zammad/issues/3075) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] - User with user_id 1 is show in admin interface (which should not) [3755](https://github.com/zammad/zammad/issues/3755) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] - Unable to close tickets in certran cases if core workflow is used [3710](https://github.com/zammad/zammad/issues/3710) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[core workflows](https://github.com/zammad/zammad/labels/core workflows)] - Login failed after upgrade to zammad 5.0 [3759](https://github.com/zammad/zammad/issues/3759) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[migration / update](https://github.com/zammad/zammad/labels/migration / update)] @@ -126,7 +193,7 @@ - Allow out of office for one day without setting two days [3590](https://github.com/zammad/zammad/issues/3590) [[bug](https://github.com/zammad/zammad/labels/bug)] [[personal settings/menu](https://github.com/zammad/zammad/labels/personal settings/menu)] - FreshDesk Import doesn't pull in auto-assign domain(s) for organizations [3687](https://github.com/zammad/zammad/issues/3687) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] - FreshDesk Import brings in all users as inactive [3689](https://github.com/zammad/zammad/issues/3689) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] -- KB Public UI icons are misspaced [3680](https://github.com/zammad/zammad/issues/3680) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)] +- KB Public UI icons are misspaced [3680](https://github.com/zammad/zammad/issues/3680) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge base)] - FreshDesk Import Error - undefined method `body' for 10:Integer [3661](https://github.com/zammad/zammad/issues/3661) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] - Cannot select multiple tickets in ticket overview with shift+click in Firefox [3449](https://github.com/zammad/zammad/issues/3449) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] - "Drop files here" drag area not always hiding [3460](https://github.com/zammad/zammad/issues/3460) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX/UI)] [[help appreciated](https://github.com/zammad/zammad/labels/help appreciated)] diff --git a/Gemfile.lock b/Gemfile.lock index 0f73602db..98ee0ecd6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -21,50 +21,50 @@ GEM specs: aasm (5.2.0) concurrent-ruby (~> 1.0) - actioncable (6.0.4.1) - actionpack (= 6.0.4.1) + actioncable (6.0.4.4) + actionpack (= 6.0.4.4) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.4.1) - actionpack (= 6.0.4.1) - activejob (= 6.0.4.1) - activerecord (= 6.0.4.1) - activestorage (= 6.0.4.1) - activesupport (= 6.0.4.1) + actionmailbox (6.0.4.4) + actionpack (= 6.0.4.4) + activejob (= 6.0.4.4) + activerecord (= 6.0.4.4) + activestorage (= 6.0.4.4) + activesupport (= 6.0.4.4) mail (>= 2.7.1) - actionmailer (6.0.4.1) - actionpack (= 6.0.4.1) - actionview (= 6.0.4.1) - activejob (= 6.0.4.1) + actionmailer (6.0.4.4) + actionpack (= 6.0.4.4) + actionview (= 6.0.4.4) + activejob (= 6.0.4.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.4.1) - actionview (= 6.0.4.1) - activesupport (= 6.0.4.1) + actionpack (6.0.4.4) + actionview (= 6.0.4.4) + activesupport (= 6.0.4.4) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.4.1) - actionpack (= 6.0.4.1) - activerecord (= 6.0.4.1) - activestorage (= 6.0.4.1) - activesupport (= 6.0.4.1) + actiontext (6.0.4.4) + actionpack (= 6.0.4.4) + activerecord (= 6.0.4.4) + activestorage (= 6.0.4.4) + activesupport (= 6.0.4.4) nokogiri (>= 1.8.5) - actionview (6.0.4.1) - activesupport (= 6.0.4.1) + actionview (6.0.4.4) + activesupport (= 6.0.4.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.4.1) - activesupport (= 6.0.4.1) + activejob (6.0.4.4) + activesupport (= 6.0.4.4) globalid (>= 0.3.6) - activemodel (6.0.4.1) - activesupport (= 6.0.4.1) - activerecord (6.0.4.1) - activemodel (= 6.0.4.1) - activesupport (= 6.0.4.1) + activemodel (6.0.4.4) + activesupport (= 6.0.4.4) + activerecord (6.0.4.4) + activemodel (= 6.0.4.4) + activesupport (= 6.0.4.4) activerecord-import (1.2.0) activerecord (>= 3.2) activerecord-nulldb-adapter (0.7.0) @@ -75,12 +75,12 @@ GEM multi_json (~> 1.11, >= 1.11.2) rack (>= 2.0.8, < 3) railties (>= 5.2.4.1) - activestorage (6.0.4.1) - actionpack (= 6.0.4.1) - activejob (= 6.0.4.1) - activerecord (= 6.0.4.1) + activestorage (6.0.4.4) + actionpack (= 6.0.4.4) + activejob (= 6.0.4.4) + activerecord (= 6.0.4.4) marcel (~> 1.0.0) - activesupport (6.0.4.1) + activesupport (6.0.4.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -250,7 +250,7 @@ GEM rainbow (>= 2.2.1) rake (>= 10.0) gli (2.20.1) - globalid (0.5.2) + globalid (1.0.0) activesupport (>= 5.0) gmail_xoauth (0.4.2) oauth (>= 0.3.6) @@ -319,7 +319,7 @@ GEM logging (2.3.0) little-plugger (~> 1.1) multi_json (~> 1.14) - loofah (2.12.0) + loofah (2.13.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) lumberjack (1.2.8) @@ -335,7 +335,7 @@ GEM mini_portile2 (2.6.1) mini_racer (0.2.9) libv8 (>= 6.9.411) - minitest (5.14.4) + minitest (5.15.0) msgpack (1.4.2) msgpack (1.4.2-x86_64-linux-musl) multi_json (1.15.0) @@ -458,20 +458,20 @@ GEM rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.4.1) - actioncable (= 6.0.4.1) - actionmailbox (= 6.0.4.1) - actionmailer (= 6.0.4.1) - actionpack (= 6.0.4.1) - actiontext (= 6.0.4.1) - actionview (= 6.0.4.1) - activejob (= 6.0.4.1) - activemodel (= 6.0.4.1) - activerecord (= 6.0.4.1) - activestorage (= 6.0.4.1) - activesupport (= 6.0.4.1) + rails (6.0.4.4) + actioncable (= 6.0.4.4) + actionmailbox (= 6.0.4.4) + actionmailer (= 6.0.4.4) + actionpack (= 6.0.4.4) + actiontext (= 6.0.4.4) + actionview (= 6.0.4.4) + activejob (= 6.0.4.4) + activemodel (= 6.0.4.4) + activerecord (= 6.0.4.4) + activestorage (= 6.0.4.4) + activesupport (= 6.0.4.4) bundler (>= 1.3.0) - railties (= 6.0.4.1) + railties (= 6.0.4.4) sprockets-rails (>= 2.0.0) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) @@ -482,9 +482,9 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.4.1) loofah (~> 2.3) - railties (6.0.4.1) - actionpack (= 6.0.4.1) - activesupport (= 6.0.4.1) + railties (6.0.4.4) + actionpack (= 6.0.4.4) + activesupport (= 6.0.4.4) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) @@ -597,9 +597,9 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.2) - actionpack (>= 4.0) - activesupport (>= 4.0) + sprockets-rails (3.4.2) + actionpack (>= 5.2) + activesupport (>= 5.2) sprockets (>= 3.0.0) sync (0.5.0) tcr (0.2.2) diff --git a/app/assets/javascripts/app/controllers/_application_controller/_base.coffee b/app/assets/javascripts/app/controllers/_application_controller/_base.coffee index 93a5c2c87..4740ece1d 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/_base.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/_base.coffee @@ -300,9 +300,10 @@ class App.Controller extends Spine.Controller frontendTimeUpdateItem: (item, currentVal) => timestamp = item.attr('datetime') - time = @humanTime(timestamp, item.hasClass('escalation')) + return if timestamp is 'null' # only do dom updates on changes + time = @humanTime(timestamp, item.hasClass('escalation')) return if time is currentVal newTitle = App.i18n.translateTimestamp(timestamp) diff --git a/app/assets/javascripts/app/controllers/_application_controller/form.coffee b/app/assets/javascripts/app/controllers/_application_controller/form.coffee index 9d968be2e..eb9d6e69d 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/form.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/form.coffee @@ -418,11 +418,11 @@ class App.ControllerForm extends App.Controller if !@constructor.fieldIsMandatory(field_by_name) field_by_name.attr('required', true) - field_by_name.parents('.form-group').find('label span').html('*') + field_by_name.closest('.form-group').find('label span').html('*') field_by_name.closest('.form-group').addClass('is-required') if !@constructor.fieldIsMandatory(field_by_data) field_by_data.attr('required', true) - field_by_data.parents('.form-group').find('label span').html('*') + field_by_data.closest('.form-group').find('label span').html('*') field_by_data.closest('.form-group').addClass('is-required') optional: (name, el = @form) -> @@ -434,11 +434,11 @@ class App.ControllerForm extends App.Controller if @constructor.fieldIsMandatory(field_by_name) field_by_name.attr('required', false) - field_by_name.parents('.form-group').find('label span').html('') + field_by_name.closest('.form-group').find('label span').html('') field_by_name.closest('.form-group').removeClass('is-required') if @constructor.fieldIsMandatory(field_by_data) field_by_data.attr('required', false) - field_by_data.parents('.form-group').find('label span').html('') + field_by_data.closest('.form-group').find('label span').html('') field_by_data.closest('.form-group').removeClass('is-required') readonly: (name, el = @form) -> diff --git a/app/assets/javascripts/app/controllers/_application_controller/table.coffee b/app/assets/javascripts/app/controllers/_application_controller/table.coffee index 7304710a0..fd6ccb6c8 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/table.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/table.coffee @@ -493,14 +493,6 @@ class App.ControllerTable extends App.Controller sortable: @dndCallback )) - getGroupByKeyName: (object, groupBy) -> - reference_key = groupBy + '_id' - - if reference_key of object - return reference_key - - groupBy - sortObjectKeys: (objects, direction) -> sorted = Object.keys(objects).sort() @@ -523,7 +515,7 @@ class App.ControllerTable extends App.Controller objectsToShow = @objectsOfPage(@pagerShownPage) if @groupBy # group by raw (and not printable) value so dates work also - objectsGrouped = _.groupBy(objectsToShow, (object) => object[@getGroupByKeyName(object, @groupBy)]) + objectsGrouped = _.groupBy(objectsToShow, (object) => @groupObjectName(object, @groupBy, excludeTags: ['date', 'datetime'])) else objectsGrouped = { '': objectsToShow } @@ -863,11 +855,15 @@ class App.ControllerTable extends App.Controller @objects = localObjects @lastSortedobjects = localObjects - groupObjectName: (object, key = undefined) -> + groupObjectName: (object, key = undefined, options = {}) -> group = object if key if key not of object key += '_id' + + # return internal value if needed + return object[key] if options.excludeTags && _.find(@attributesList, (attr) -> attr.name == key && _.contains(options.excludeTags, attr.tag)) + group = App.viewPrint(object, key, @attributesList) if _.isEmpty(group) group = '' diff --git a/app/assets/javascripts/app/controllers/_application_controller/technical_error_modal.coffee b/app/assets/javascripts/app/controllers/_application_controller/technical_error_modal.coffee new file mode 100644 index 000000000..6df18b7cd --- /dev/null +++ b/app/assets/javascripts/app/controllers/_application_controller/technical_error_modal.coffee @@ -0,0 +1,9 @@ +class App.ControllerTechnicalErrorModal extends App.ControllerModal + head: "StatusCode: #{status}" + contentCode: '' + buttonClose: false + buttonSubmit: 'Ok' + onSubmit: (e) -> @close(e) + + content: -> + "
#{@contentCode}
" diff --git a/app/assets/javascripts/app/controllers/_channel/email.coffee b/app/assets/javascripts/app/controllers/_channel/email.coffee index 2fd61a432..f0920d1bb 100644 --- a/app/assets/javascripts/app/controllers/_channel/email.coffee +++ b/app/assets/javascripts/app/controllers/_channel/email.coffee @@ -428,20 +428,7 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal ui.hide('options::folder') ui.hide('options::keep_on_server') - handlePort = (params, attribute, attributes, classname, form, ui) -> - return if !params - return if !params.options - currentPort = @$('[name="options::port"]').val() - if params.options.ssl is true - if !currentPort || currentPort is '143' - @$('[name="options::port"]').val('993') - return - if params.options.ssl is false - if !currentPort || currentPort is '993' - @$('[name="options::port"]').val('143') - return - - new App.ControllerForm( + form = new App.ControllerForm( el: @$('.base-inbound-settings'), model: configure_attributes: configureAttributesInbound @@ -449,10 +436,16 @@ class ChannelEmailAccountWizard extends App.ControllerWizardModal params: @account.inbound handlers: [ showHideFolder, - handlePort, ] ) + form.el.find("select[name='options::ssl']").off('change').on('change', (e) -> + if $(e.target).val() is 'true' + form.el.find("[name='options::port']").val('993') + else + form.el.find("[name='options::port']").val('143') + ) + toggleOutboundAdapter: => # fill user / password based on intro info diff --git a/app/assets/javascripts/app/controllers/_integration/placetel.coffee b/app/assets/javascripts/app/controllers/_integration/placetel.coffee index 8289af1ad..72848885d 100644 --- a/app/assets/javascripts/app/controllers/_integration/placetel.coffee +++ b/app/assets/javascripts/app/controllers/_integration/placetel.coffee @@ -60,6 +60,30 @@ class Form extends App.Controller placetel_token: App.Setting.get('placetel_token') ) + configure_attributes = [ + { + name: 'view_limit', + display: '', + tag: 'select', + null: false, + options: [ + { name: 60, value: 60 } + { name: 120, value: 120 } + { name: 180, value: 180 } + { name: 240, value: 240 } + { name: 300, value: 300 } + ] + }, + ] + new App.ControllerForm( + el: @$('.js-viewLimit') + model: + configure_attributes: configure_attributes, + params: + view_limit: @config['view_limit'] + autofocus: false + ) + updateCurrentConfig: => config = @config cleanupInput = @cleanupInput @@ -70,6 +94,10 @@ class Form extends App.Controller default_caller_id = @$('input[name=default_caller_id]').val() config.outbound.default_caller_id = cleanupInput(default_caller_id) + # default view limit + view_limit = @$('select[name=view_limit]').val() + config.view_limit = parseInt(view_limit) + # routing table config.outbound.routing_table = [] @$('.js-outboundRouting .js-row').each(-> diff --git a/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee index 8c7ee0a2d..63bf6e2b1 100644 --- a/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee +++ b/app/assets/javascripts/app/controllers/_integration/sipgate_io.coffee @@ -59,17 +59,42 @@ class Form extends App.Controller config: @config ) + configure_attributes = [ + { + name: 'view_limit', + display: '', + tag: 'select', + null: false, + options: [ + { name: 60, value: 60 } + { name: 120, value: 120 } + { name: 180, value: 180 } + { name: 240, value: 240 } + { name: 300, value: 300 } + ] + }, + ] + new App.ControllerForm( + el: @$('.js-viewLimit') + model: + configure_attributes: configure_attributes, + params: + view_limit: @config['view_limit'] + autofocus: false + ) + updateCurrentConfig: => config = @config cleanupInput = @cleanupInput - config.api_user = cleanupInput(@$('input[name=api_user]').val()) - config.api_password = cleanupInput(@$('input[name=api_password]').val()) - # default caller_id default_caller_id = @$('input[name=default_caller_id]').val() config.outbound.default_caller_id = cleanupInput(default_caller_id) + # default view limit + view_limit = @$('select[name=view_limit]').val() + config.view_limit = parseInt(view_limit) + # routing table config.outbound.routing_table = [] @$('.js-outboundRouting .js-row').each(-> diff --git a/app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee b/app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee index 5263f59e7..3db58527f 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee @@ -96,6 +96,8 @@ class App.UiElement.ApplicationSelector # ignore passwords and relations if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false config = _.clone(row) + if config.tag is 'textarea' + config.expanding = false if config.type is 'email' || config.type is 'tel' config.type = 'text' for operatorRegEx, operator of operators_type diff --git a/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee index 9c48d193b..4a950bc02 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/_application_ui_element.coffee @@ -46,7 +46,7 @@ class App.UiElement.ApplicationUiElement result = [] for row in selection if attribute.translate - row.name = App.i18n.translateInline(row.name) + row.name = App.i18n.translatePlain(row.name) if !_.isEmpty(row.children) row.children = @getConfigOptionListArray(attribute, row.children) result.push row @@ -65,7 +65,7 @@ class App.UiElement.ApplicationUiElement for key in order name_new = selection[key] if attribute.translate - name_new = App.i18n.translateInline(name_new) + name_new = App.i18n.translatePlain(name_new) attribute.options.push { name: name_new value: key @@ -162,7 +162,7 @@ class App.UiElement.ApplicationUiElement nameNew = item.name if attribute.translate - nameNew = App.i18n.translateInline(nameNew) + nameNew = App.i18n.translatePlain(nameNew) row = value: item.id, diff --git a/app/assets/javascripts/app/controllers/_ui_element/auth_provider.coffee b/app/assets/javascripts/app/controllers/_ui_element/auth_provider.coffee new file mode 100644 index 000000000..07900dedd --- /dev/null +++ b/app/assets/javascripts/app/controllers/_ui_element/auth_provider.coffee @@ -0,0 +1,9 @@ +# coffeelint: disable=camel_case_classes +class App.UiElement.auth_provider + @render: (attribute) -> + for key, value of App.Config.get('auth_provider_all') + continue if value.config isnt attribute.provider + attribute.value = "#{App.Config.get('http_type')}://#{App.Config.get('fqdn')}#{value.url}/callback" + break + + $( App.view('generic/auth_provider')( attribute: attribute ) ) diff --git a/app/assets/javascripts/app/controllers/_ui_element/basedate.coffee b/app/assets/javascripts/app/controllers/_ui_element/basedate.coffee index bdbb61b15..864fdd0d7 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/basedate.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/basedate.coffee @@ -155,4 +155,4 @@ class App.UiElement.basedate clear: 'clear' } - App.i18n.translateDeep(data) + App.i18n.translateDeepPlain(data) diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee index 6cedd5b90..011615fc7 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee @@ -50,15 +50,15 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel operatorsType = 'active$': ['is'] - 'boolean$': ['is', 'is not', 'is set', 'not set'] - 'integer$': ['is', 'is not', 'is set', 'not set'] - '^select$': ['is', 'is not', 'is set', 'not set'] - '^tree_select$': ['is', 'is not', 'is set', 'not set'] - '^(input|textarea|richtext)$': ['is', 'is not', 'is set', 'not set', 'regex match', 'regex mismatch'] + 'boolean$': ['is', 'is not', 'is set', 'not set', 'has changed', 'changed to'] + 'integer$': ['is', 'is not', 'is set', 'not set', 'has changed', 'changed to'] + '^select$': ['is', 'is not', 'is set', 'not set', 'has changed', 'changed to'] + '^tree_select$': ['is', 'is not', 'is set', 'not set', 'has changed', 'changed to'] + '^(input|textarea|richtext)$': ['is', 'is not', 'is set', 'not set', 'has changed', 'changed to', 'regex match', 'regex mismatch'] operatorsName = - '_id$': ['is', 'is not', 'is set', 'not set'] - '_ids$': ['is', 'is not', 'is set', 'not set'] + '_id$': ['is', 'is not', 'is set', 'not set', 'has changed', 'changed to'] + '_ids$': ['is', 'is not', 'is set', 'not set', 'has changed', 'changed to'] # merge config elements = {} @@ -153,6 +153,8 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel # ignore passwords and relations if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids' && row.searchable isnt false config = _.clone(row) + if config.tag is 'textarea' + config.expanding = false if config.tag is 'select' config.multiple = true config.default = undefined @@ -175,7 +177,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel currentOperator = elementRow.find('.js-operator option:selected').attr('value') name = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) - if _.contains(['is set', 'not set'], currentOperator) + if _.contains(['is set', 'not set', 'has changed'], currentOperator) elementRow.find('.js-value').addClass('hide').html('') return diff --git a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee index 1e1969ff7..7ac20494c 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee @@ -128,7 +128,7 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) @buildValueConfigMultiple: (config, meta) -> - if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator) + if _.contains(['add_option', 'remove_option', 'set_fixed_to', 'select'], meta.operator) config.multiple = true config.nulloption = true else diff --git a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee index 316515828..5212bc356 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee @@ -244,7 +244,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi params: params ) configureAttributes = [ - { name: 'data_option::diff', display: 'Default time Diff (minutes)', tag: 'integer', null: false, default: 24 }, + { name: 'data_option::diff', display: 'Default time Diff (minutes)', tag: 'integer', null: true }, ] datetimeDiff = new App.ControllerForm( model: @@ -258,7 +258,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi @date: (item, localParams, params) -> configureAttributes = [ - { name: 'data_option::diff', display: 'Default time Diff (hours)', tag: 'integer', null: false, default: 24 }, + { name: 'data_option::diff', display: 'Default time Diff (hours)', tag: 'integer', null: true }, ] dateDiff = new App.ControllerForm( model: diff --git a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee index b7b027ae7..757ebaa03 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/richtext.coffee @@ -27,6 +27,8 @@ class App.UiElement.richtext renderFile = (file) -> item.find('.attachments').append(App.view('generic/attachment_item')(file)) attachments.push file + if form.richTextUploadRenderCallback + form.richTextUploadRenderCallback(attribute, attachments) if params && params.attachments for file in params.attachments @@ -54,6 +56,8 @@ class App.UiElement.richtext return if item.id.toString() is id.toString() item ) + if form.richTextUploadDeleteCallback + form.richTextUploadDeleteCallback(attribute, attachments) form_id = item.closest('form').find('[name=form_id]').val() diff --git a/app/assets/javascripts/app/controllers/_ui_element/sla_times.coffee b/app/assets/javascripts/app/controllers/_ui_element/sla_times.coffee index 1a3fec95c..2bf72247d 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/sla_times.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/sla_times.coffee @@ -8,9 +8,11 @@ class App.UiElement.sla_times item = $( App.view('generic/sla_times')( attribute: attribute first_response_time: params.first_response_time + response_time: params.response_time update_time: params.update_time solution_time: params.solution_time first_response_time_in_text: @toText(params.first_response_time) + response_time_in_text: @toText(params.response_time) update_time_in_text: @toText(params.update_time) solution_time_in_text: @toText(params.solution_time) ) ) @@ -26,12 +28,16 @@ class App.UiElement.sla_times row = element.closest('tr') if element.prop('checked') row.addClass('is-active') + + if row.has('.js-updateTypeSelector').length > 0 && row.has('.js-updateTypeSelector:checked').length == 0 + row.find('.js-updateTypeSelector:first').prop('checked', true) else row.removeClass('is-active') # reset data item row.find('.js-timeConvertFrom').val('') row.find('.js-timeConvertTo').val('') + row.find('.js-updateTypeSelector').attr('checked', false) row.find('.help-inline').empty() row.removeClass('has-error') ) @@ -42,12 +48,16 @@ class App.UiElement.sla_times inText = element.val() row = element.closest('tr') - row.find('.js-activateRow').prop('checked', true) + + row + .find('.js-activateRow') + .prop('checked', true) + .trigger('change') + row.addClass('is-active') - element - .closest('td') - .find('.js-timeConvertTo') + row + .find("[name='#{element.data('name')}']") .val(@toMinutes(inText) || '') ) @@ -56,9 +66,19 @@ class App.UiElement.sla_times $(e.currentTarget).closest('tr').find('.checkbox-replacement').click() ) + # toggle update type on clicking around the element + item.find('.js-forward-radio').bind('click', (e) -> + elem = $(e.currentTarget).closest('p').find('.js-updateTypeSelector') + + elem.prop('checked', true) + elem.trigger('change') + ) + # focus time input on clicking surrounding cell item.find('.js-focus-input').bind('click', (e) -> - $(e.currentTarget).find('.form-control').focus() + $(e.currentTarget) + .find('.form-control:visible') + .focus() ) # show placeholder instead of 00:00 @@ -67,15 +87,36 @@ class App.UiElement.sla_times $(e.currentTarget).val('') ) + # switch update/response times when type is selected accordingly + item.find('.js-updateTypeSelector').bind('change', (e) -> + element = $(e.target) + row = element.closest('tr') + row.find('.js-activateRow').prop('checked', true) + row.addClass('is-active') + + row + .find('.js-timeConvertFrom') + .addClass('hidden') + .val('') + + row + .find('.js-timeConvertTo') + .val('') + + row + .find("[data-name='#{element.val()}_time']") + .removeClass('hidden') + ) + # set initial active/inactive rows item.find('.js-timeConvertFrom').each(-> row = $(@).closest('tr') checkbox = row.find('.js-activateRow') - if $(@).val() - checkbox.prop('checked', true) - row.addClass('is-active') - else - checkbox.prop('checked', false) + + return if !$(@).val() + + checkbox.prop('checked', true) + row.addClass('is-active') ) item diff --git a/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee b/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee index 3987bd43f..4ed6e555f 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/textarea.coffee @@ -4,16 +4,17 @@ class App.UiElement.textarea fileUploaderId = 'file-uploader-' + new Date().getTime() + '-' + Math.floor( Math.random() * 99999 ) item = $( App.view('generic/textarea')( attribute: attribute ) + '
' ) - a = -> - visible = $( item[0] ).is(':visible') - if visible && !$( item[0] ).expanding('active') - $( item[0] ).expanding() - $( item[0] ).on('focus', -> + if attribute.expanding isnt false + a = -> visible = $( item[0] ).is(':visible') if visible && !$( item[0] ).expanding('active') - $( item[0] ).expanding().focus() - ) - App.Delay.set(a, 80) + $( item[0] ).expanding() + $( item[0] ).on('focus', -> + visible = $( item[0] ).is(':visible') + if visible && !$( item[0] ).expanding('active') + $( item[0] ).expanding().focus() + ) + App.Delay.set(a, 80) if attribute.upload @@ -41,4 +42,4 @@ class App.UiElement.textarea debug: false ) App.Delay.set(u, 100, undefined, 'form_upload') - item \ No newline at end of file + item diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee index 954aa8859..24386d445 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.coffee @@ -48,12 +48,15 @@ class App.TicketCreate extends App.Controller if @ticket_id && @article_id @split = "/#{@ticket_id}/#{@article_id}" - load = (data) => - App.Collection.loadAssets(data.assets) - @formMeta = data.form_meta - @buildScreen(params) - @bindId = App.TicketCreateCollection.bind(load, false) - App.TicketCreateCollection.fetch() + @ajax( + type: 'GET' + url: "#{@apiPath}/ticket_create" + processData: true + success: (data, status, xhr) => + App.Collection.loadAssets(data.assets) + @formMeta = data.form_meta + @buildScreen(params) + ) # rerender view, e. g. on langauge change @controllerBind('ui:rerender', => @@ -69,9 +72,6 @@ class App.TicketCreate extends App.Controller @sidebarWidget.render(@params()) ) - release: => - App.TicketCreateCollection.unbindById(@bindId) - currentChannel: => if !type type = @$('.type-tabs .tab.active').data('type') @@ -282,8 +282,15 @@ class App.TicketCreate extends App.Controller return if !@formMeta App.QueueManager.run(@queueKey) + updateTaskManagerAttachments: (attribute, attachments) => + taskData = App.TaskManager.get(@taskKey) + return if _.isEmpty(taskData) + + taskData.attachments = attachments + App.TaskManager.update(@taskKey, taskData) + render: (template = {}) -> - return if !@formMeta + # get params params = @prefilledParams || {} if template && !_.isEmpty(template.options) @@ -325,17 +332,16 @@ class App.TicketCreate extends App.Controller handlers = @Config.get('TicketCreateFormHandler') @controllerFormCreateMiddle = new App.ControllerForm( - el: @$('.ticket-form-middle') - form_id: @formId - model: App.Ticket - screen: 'create_middle' - handlersConfig: handlers - filter: @formMeta.filter - formMeta: @formMeta - params: params - noFieldset: true - taskKey: @taskKey - rejectNonExistentValues: true + el: @$('.ticket-form-middle') + form_id: @formId + model: App.Ticket + screen: 'create_middle' + handlersConfig: handlers + formMeta: @formMeta + params: params + noFieldset: true + taskKey: @taskKey + rejectNonExistentValues: true ) # tunnel events to make sure core workflow does know @@ -359,8 +365,6 @@ class App.TicketCreate extends App.Controller events: 'change [name=customer_id]': @localUserInfo handlersConfig: handlersTunnel - filter: @formMeta.filter - formMeta: @formMeta autofocus: true params: params taskKey: @taskKey @@ -376,6 +380,8 @@ class App.TicketCreate extends App.Controller handlersConfig: handlersTunnel params: params taskKey: @taskKey + richTextUploadRenderCallback: @updateTaskManagerAttachments + richTextUploadDeleteCallback: @updateTaskManagerAttachments ) @controllerFormCreateBottom = new App.ControllerForm( el: @$('.ticket-form-bottom') @@ -383,8 +389,6 @@ class App.TicketCreate extends App.Controller model: App.Ticket screen: 'create_bottom' handlersConfig: handlersTunnel - filter: @formMeta.filter - formMeta: @formMeta params: params taskKey: @taskKey ) diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create/form_hander_signature.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create/form_handler_signature.coffee similarity index 77% rename from app/assets/javascripts/app/controllers/agent_ticket_create/form_hander_signature.coffee rename to app/assets/javascripts/app/controllers/agent_ticket_create/form_handler_signature.coffee index f5c2ad89e..25da0b346 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create/form_hander_signature.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create/form_handler_signature.coffee @@ -1,4 +1,4 @@ -class TicketCreateFormHanderSignature +class TicketCreateFormHandlerSignature @run: (params, attribute, attributes, classname, form, ui) -> return if !attribute @@ -19,10 +19,7 @@ class TicketCreateFormHanderSignature if App.Utils.signatureCheck(currentBody.html() || '', signatureFinished) # if signature has changed, in case remove old signature - signature_id = ui.el.closest('.content').find('[data-signature=true]').data('signature-id') - if signature_id && signature_id.toString() isnt signature.id.toString() - - ui.el.closest('.content').find('[data-signature="true"]').remove() + ui.el.closest('.content').find('[data-signature="true"]').remove() if !App.Utils.htmlLastLineEmpty(currentBody) currentBody.append('

') @@ -35,4 +32,4 @@ class TicketCreateFormHanderSignature else ui.el.closest('.content').find('[data-name="body"]').find('[data-signature=true]').remove() -App.Config.set('200-ticketFormSignature', TicketCreateFormHanderSignature, 'TicketCreateFormHandler') +App.Config.set('200-ticketFormSignature', TicketCreateFormHandlerSignature, 'TicketCreateFormHandler') diff --git a/app/assets/javascripts/app/controllers/customer_ticket_create.coffee b/app/assets/javascripts/app/controllers/customer_ticket_create.coffee index c09dfdc8e..d8f9bacd5 100644 --- a/app/assets/javascripts/app/controllers/customer_ticket_create.coffee +++ b/app/assets/javascripts/app/controllers/customer_ticket_create.coffee @@ -13,13 +13,7 @@ class CustomerTicketCreate extends App.ControllerAppContent @form_id = App.ControllerForm.formId() @navupdate '#customer_ticket_new' - - load = (data) => - App.Collection.loadAssets(data.assets) - @formMeta = data.form_meta - @render() - @bindId = App.TicketCreateCollection.bind(load, false) - App.TicketCreateCollection.fetch() + @render() render: (template = {}) -> if !@Config.get('customer_ticket_create') @@ -43,8 +37,6 @@ class CustomerTicketCreate extends App.ControllerAppContent form_id: @form_id model: App.Ticket screen: 'create_middle' - filter: @formMeta.filter - formMeta: @formMeta params: defaults noFieldset: true handlersConfig: handlers @@ -70,8 +62,6 @@ class CustomerTicketCreate extends App.ControllerAppContent model: App.Ticket screen: 'create_top' handlersConfig: handlersTunnel - filter: @formMeta.filter - formMeta: @formMeta autofocus: true params: defaults ) @@ -83,8 +73,6 @@ class CustomerTicketCreate extends App.ControllerAppContent events: 'fileUploadStart .richtext': => @submitDisable() 'fileUploadStop .richtext': => @submitEnable() - filter: @formMeta.filter - formMeta: @formMeta params: defaults handlersConfig: handlersTunnel ) @@ -95,8 +83,6 @@ class CustomerTicketCreate extends App.ControllerAppContent model: App.Ticket screen: 'create_bottom' handlersConfig: handlersTunnel - filter: @formMeta.filter - formMeta: @formMeta params: defaults ) diff --git a/app/assets/javascripts/app/controllers/getting_started/channel_email.coffee b/app/assets/javascripts/app/controllers/getting_started/channel_email.coffee index d698b64b4..d5c07050c 100644 --- a/app/assets/javascripts/app/controllers/getting_started/channel_email.coffee +++ b/app/assets/javascripts/app/controllers/getting_started/channel_email.coffee @@ -91,20 +91,7 @@ class GettingStartedChannelEmail extends App.ControllerWizardFullScreen ui.hide('options::folder') ui.hide('options::keep_on_server') - handlePort = (params, attribute, attributes, classname, form, ui) -> - return if !params - return if !params.options - currentPort = @$('.base-inbound-settings [name="options::port"]').val() - if params.options.ssl is true - if !currentPort - @$('.base-inbound-settings [name="options::port"]').val('993') - return - if params.options.ssl is false - if !currentPort || currentPort is '993' - @$('.base-inbound-settings [name="options::port"]').val('143') - return - - new App.ControllerForm( + form = new App.ControllerForm( el: @$('.base-inbound-settings') model: configure_attributes: configureAttributesInbound @@ -112,10 +99,16 @@ class GettingStartedChannelEmail extends App.ControllerWizardFullScreen params: @account.inbound handlers: [ showHideFolder, - handlePort, ] ) + form.el.find("select[name='options::ssl']").off('change').on('change', (e) -> + if $(e.target).val() is 'true' + form.el.find("[name='options::port']").val('993') + else + form.el.find("[name='options::port']").val('143') + ) + toggleOutboundAdapter: => # fill user / password based on intro info diff --git a/app/assets/javascripts/app/controllers/import_freshdesk.coffee b/app/assets/javascripts/app/controllers/import_freshdesk.coffee index 42b16e066..910aec332 100644 --- a/app/assets/javascripts/app/controllers/import_freshdesk.coffee +++ b/app/assets/javascripts/app/controllers/import_freshdesk.coffee @@ -165,7 +165,7 @@ class ImportFreshdesk extends App.ControllerWizardFullScreen @$('.js-error').addClass('hide') if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error']) - window.location.reload() + @redirectToLogin() return if !_.isEmpty(data.result) diff --git a/app/assets/javascripts/app/controllers/import_zendesk.coffee b/app/assets/javascripts/app/controllers/import_zendesk.coffee index a9ac9932b..8d982a5b5 100644 --- a/app/assets/javascripts/app/controllers/import_zendesk.coffee +++ b/app/assets/javascripts/app/controllers/import_zendesk.coffee @@ -163,7 +163,7 @@ class ImportZendesk extends App.ControllerWizardFullScreen @$('.js-error').addClass('hide') if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error']) - window.location.reload() + @redirectToLogin() return if !_.isEmpty(data.result) diff --git a/app/assets/javascripts/app/controllers/object_manager.coffee b/app/assets/javascripts/app/controllers/object_manager.coffee index f3f8e03a2..8582f8c4d 100644 --- a/app/assets/javascripts/app/controllers/object_manager.coffee +++ b/app/assets/javascripts/app/controllers/object_manager.coffee @@ -247,8 +247,6 @@ class Edit extends App.ControllerGenericEdit #if attribute.name is 'data_type' # attribute.disabled = true - console.log('configure_attributes', configure_attributes) - @controller = new App.ControllerForm( model: configure_attributes: configure_attributes diff --git a/app/assets/javascripts/app/controllers/organization_profile.coffee b/app/assets/javascripts/app/controllers/organization_profile.coffee index 65db8b5c1..ad7890c68 100644 --- a/app/assets/javascripts/app/controllers/organization_profile.coffee +++ b/app/assets/javascripts/app/controllers/organization_profile.coffee @@ -153,7 +153,7 @@ class Object extends App.ControllerObserver elLocal.find('.js-userList').html(members) ) - if @organization.member_ids.length < @memberLimit + if @organization.member_ids.length <= @memberLimit @el.find('.js-showMoreMembers').parent().addClass('hidden') else @el.find('.js-showMoreMembers').parent().removeClass('hidden') diff --git a/app/assets/javascripts/app/controllers/report.coffee b/app/assets/javascripts/app/controllers/report.coffee index e839112ab..e2630ae06 100644 --- a/app/assets/javascripts/app/controllers/report.coffee +++ b/app/assets/javascripts/app/controllers/report.coffee @@ -171,6 +171,13 @@ class Graph extends App.Controller backends: @params.backendSelected ) processData: true + error: (xhr) => + return if !_.include([401, 403, 404, 422, 502], xhr.status) + + @bodyModal = new App.ControllerTechnicalErrorModal( + head: 'Cannot generate report' + contentCode: xhr.responseJSON.error + ) success: (data) => @update(data) @delay(@render, interval, 'report-update', 'page') diff --git a/app/assets/javascripts/app/controllers/search.coffee b/app/assets/javascripts/app/controllers/search.coffee index 0cebfb26e..ac17d6930 100644 --- a/app/assets/javascripts/app/controllers/search.coffee +++ b/app/assets/javascripts/app/controllers/search.coffee @@ -56,13 +56,9 @@ class App.Search extends App.Controller @navupdate(url: '#search', type: 'menu') return if _.isEmpty(params.query) - @$('.js-search').val(params.query).trigger('change') - return if @shown - - @search(1000, true) + @$('.js-search').val(params.query).trigger('keyup') hide: -> - @shown = false if @table @table.hide() @@ -108,6 +104,7 @@ class App.Search extends App.Controller return # on other keys, show result + @navigate "#search/#{encodeURIComponent(@searchInput.val())}" @search(0) empty: => diff --git a/app/assets/javascripts/app/controllers/sla.coffee b/app/assets/javascripts/app/controllers/sla.coffee index b3f50e1d3..db67c2cc3 100644 --- a/app/assets/javascripts/app/controllers/sla.coffee +++ b/app/assets/javascripts/app/controllers/sla.coffee @@ -26,12 +26,6 @@ class Sla extends App.ControllerSubContent sortBy: 'name' ) for sla in slas - if sla.first_response_time - sla.first_response_time_in_text = @toText(sla.first_response_time) - if sla.update_time - sla.update_time_in_text = @toText(sla.update_time) - if sla.solution_time - sla.solution_time_in_text = @toText(sla.solution_time) sla.rules = App.UiElement.ticket_selector.humanText(sla.condition) sla.calendar = App.Calendar.find(sla.calendar_id) @@ -95,21 +89,8 @@ class Sla extends App.ControllerSubContent description: (e) => new App.ControllerGenericDescription( - description: App.Calendar.description + description: App.Sla.description container: @el.closest('.content') ) - toText: (m) -> - m = parseInt(m) - return if !m - minutes = m % 60 - hours = Math.floor(m / 60) - - if minutes < 10 - minutes = "0#{minutes}" - if hours < 10 - hours = "0#{hours}" - - "#{hours}:#{minutes}" - App.Config.set('Sla', { prio: 2900, name: 'SLAs', parent: '#manage', target: '#manage/slas', controller: Sla, permission: ['admin.sla'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index aa254c6e3..e017c7f82 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -206,22 +206,13 @@ class App.TicketOverview extends App.Controller article: article ) ticket.article = article - ticket.ajax().update( - ticket.attributes() - # this option will prevent callbacks and invalid data states in case of an error - failResponseNoTrigger: true + ticket.save( done: (r) => @batchCountIndex++ # refresh view after all tickets are proceeded if @batchCountIndex == @batchCount App.Event.trigger('overview:fetch') - fail: (record, settings, details) -> - console.log('record, settings, details', record, settings, details) - App.Event.trigger('notify', { - type: 'error' - msg: App.i18n.translateContent('Bulk action stopped %s!', error) - }) ) return @@ -234,21 +225,13 @@ class App.TicketOverview extends App.Controller ticket.owner_id = id if !_.isEmpty(groupId) ticket.group_id = groupId - ticket.ajax().update( - ticket.attributes() - # this option will prevent callbacks and invalid data states in case of an error - failResponseNoTrigger: true + ticket.save( done: (r) => @batchCountIndex++ # refresh view after all tickets are proceeded if @batchCountIndex == @batchCount App.Event.trigger('overview:fetch') - fail: (record, settings, details) -> - App.Event.trigger('notify', { - type: 'error' - msg: App.i18n.translateContent('Bulk action stopped %s!', settings.error) - }) ) return @@ -259,21 +242,13 @@ class App.TicketOverview extends App.Controller #console.log "perform action #{action} with id #{id} on ", $(item).val() ticket = App.Ticket.find($(item).val()) ticket.group_id = id - ticket.ajax().update( - ticket.attributes() - # this option will prevent callbacks and invalid data states in case of an error - failResponseNoTrigger: true + ticket.save( done: (r) => @batchCountIndex++ # refresh view after all tickets are proceeded if @batchCountIndex == @batchCount App.Event.trigger('overview:fetch') - fail: (record, settings, details) -> - App.Event.trigger('notify', { - type: 'error' - msg: App.i18n.translateContent('Bulk action stopped %s!', error) - }) ) return diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index 6b560691b..5c32d7037 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -493,16 +493,22 @@ class App.TicketZoom extends App.Controller @form_id = @taskGet('article').form_id || App.ControllerForm.formId() @articleNew = new App.TicketZoomArticleNew( - ticket: @ticket - ticket_id: @ticket_id - el: elLocal.find('.article-new') - formMeta: @formMeta - form_id: @form_id - defaults: @taskGet('article') - taskKey: @taskKey - ui: @ - callbackFileUploadStart: @submitDisable - callbackFileUploadStop: @submitEnable + ticket: @ticket + ticket_id: @ticket_id + el: elLocal.find('.article-new') + formMeta: @formMeta + form_id: @form_id + defaults: @taskGet('article') + taskKey: @taskKey + ui: @ + richTextUploadStartCallback: @submitDisable + richTextUploadRenderCallback: (attachments) => + @submitEnable() + @taskUpdateAttachments('article', attachments) + @delay(@markForm, 250, 'ticket-zoom-form-update') + richTextUploadDeleteCallback: (attachments) => + @taskUpdateAttachments('article', attachments) + @delay(@markForm, 250, 'ticket-zoom-form-update') ) @highligher = new App.TicketZoomHighlighter( @@ -641,6 +647,7 @@ class App.TicketZoom extends App.Controller # update changes in ui currentStore = @currentStore() modelDiff = @formDiff(currentParams, currentStore) + return if _.isEmpty(modelDiff) # set followup state if needed @setDefaultFollowUpState(modelDiff, currentStore) @@ -720,7 +727,7 @@ class App.TicketZoom extends App.Controller # add attachments if exist attachmentCount = @$('.article-add .textBubble .attachments .attachment').length if attachmentCount > 0 - currentParams.article.attachments = true + currentParams.article.attachments = attachmentCount else delete currentParams.article.attachments @@ -735,6 +742,14 @@ class App.TicketZoom extends App.Controller # do not compare null or undefined value if currentStore.ticket + + # make sure that the compared state is same in local storage and + # rendered html. Else we could have race conditions of data + # which is not rendered yet + renderedUpdatedAt = @el.find('.edit').attr('data-ticket-updated-at') + return if !renderedUpdatedAt + return if currentStore.ticket.updated_at.toString() isnt renderedUpdatedAt + for key, value of currentStore.ticket if value is null || value is undefined currentStore.ticket[key] = '' @@ -969,15 +984,14 @@ class App.TicketZoom extends App.Controller processData: true success: (data) => - #App.SessionStorage.set(@key, data) - @load(data, true, true) - # reset article - should not be resubmitted on next ticket update ticket.article = undefined # reset form after save @reset() + @load(data, false, true) + if @sidebarWidget @sidebarWidget.commit() @@ -1070,6 +1084,13 @@ class App.TicketZoom extends App.Controller App.TaskManager.update(@taskKey, taskData) + taskUpdateAttachments: (area, attachments) => + taskData = App.TaskManager.get(@taskKey) + return if !taskData + + taskData.attachments = attachments + App.TaskManager.update(@taskKey, taskData) + taskUpdateAll: (data) => @localTaskData = data @localTaskData.article['form_id'] = @form_id @@ -1092,7 +1113,7 @@ class App.TicketZoom extends App.Controller @localTaskData = ticket: {} article: {} - App.TaskManager.update(@taskKey, { 'state': @localTaskData }) + App.TaskManager.update(@taskKey, { 'state': @localTaskData, attachments: [] }) renderOverviewNavigator: (parentEl) -> new App.TicketZoomOverviewNavigator( diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee index e8802b8e3..cda0a1d26 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/article_new.coffee @@ -198,17 +198,17 @@ class App.TicketZoomArticleNew extends App.Controller inputField: @$('.article-attachment input') onFileStartCallback: => - @callbackFileUploadStart?() + @richTextUploadStartCallback?() onFileCompletedCallback: (response) => @attachments.push response.data @renderAttachment(response.data) @$('.article-attachment input').val('') - @callbackFileUploadStop?() + @richTextUploadRenderCallback?(@attachments) onFileAbortedCallback: => - @callbackFileUploadStop?() + @richTextUploadRenderCallback?(@attachments) attachmentPlaceholder: @attachmentPlaceholder attachmentUpload: @attachmentUpload @@ -287,7 +287,6 @@ class App.TicketZoomArticleNew extends App.Controller params.preferences ||= {} params.preferences.security = @paramsSecurity() - params.attachments = @attachments params validate: => @@ -624,6 +623,8 @@ class App.TicketZoomArticleNew extends App.Controller $(e.currentTarget).closest('.attachment').remove() if element.find('.attachment').length == 0 element.empty() + + @richTextUploadDeleteCallback?(@attachments) ) actions: -> diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee index d0ca39503..6c4158f34 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/form_handler_core_workflow.coffee @@ -79,6 +79,12 @@ class App.FormHandlerCoreWorkflow return if !App.WebSocket.channel() return !App.Config.get('core_workflow_ajax_mode') + @restrictValuesAttributeCache: (attribute, values) -> + result = { values: values } + return result if !attribute.relation + result.lastUpdatedAt = App[attribute.relation].lastUpdatedAt() + return result + # restricts the dropdown and tree select values of a form @restrictValues: (classname, form, ui, attributes, params, data) -> return if _.isEmpty(data.restrict_values) @@ -111,17 +117,17 @@ class App.FormHandlerCoreWorkflow # cache state for performance and only run # if values or param differ if coreWorkflowRestrictions?[classname]?[item.name] - compare = values + compare = App.FormHandlerCoreWorkflow.restrictValuesAttributeCache(attribute, values) continue if _.isEqual(coreWorkflowRestrictions[classname][item.name], compare) coreWorkflowRestrictions[classname] ||= {} - coreWorkflowRestrictions[classname][item.name] = values + coreWorkflowRestrictions[classname][item.name] = App.FormHandlerCoreWorkflow.restrictValuesAttributeCache(attribute, values) valueFound = false for value in values # false values are valid values e.g. for boolean fields (be careful) - if value isnt undefined && paramValue isnt undefined + if value isnt undefined && paramValue isnt undefined && value isnt null && paramValue isnt null if value.toString() == paramValue.toString() valueFound = true break @@ -133,6 +139,11 @@ class App.FormHandlerCoreWorkflow if valueFound item.default = paramValue item.newValue = paramValue + else if params.id + obj = App[ui.model.className].find(params.id) + if obj && obj[item.name] + item.default = obj[item.name] + item.newValue = obj[item.name] else item.default = '' item.newValue = '' @@ -299,6 +310,11 @@ class App.FormHandlerCoreWorkflow screen: ui.screen } + # send last changed attribute only once for has changed condition + if ui.lastChangedAttribute + requestData.last_changed_attribute = ui.lastChangedAttribute + ui.lastChangedAttribute = '-' + if App.FormHandlerCoreWorkflow.useWebSockets() App.WebSocket.send(requestData) else @@ -324,8 +340,12 @@ class App.FormHandlerCoreWorkflow # get params and add id from ui if needed params = App.FormHandlerCoreWorkflow.cleanParams(params_ref) - if ui?.params?.id + + # add object id for edit screens + if ui?.params?.id && ui.screen.match(/edit/) params.id = ui.params.id + else + delete params.id # skip double checks return if _.isEqual(coreWorkflowParams[classname], params) diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee index bdb18819b..4b516e209 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee @@ -29,41 +29,33 @@ class Edit extends App.Controller # for the new ticket + eventually changed task state @formMeta.core_workflow = undefined - if followUpPossible == 'new_ticket' && ticketState != 'closed' || - followUpPossible != 'new_ticket' || - @permissionCheck('admin') || @ticket.currentView() is 'agent' - @controllerFormSidebarTicket = new App.ControllerForm( - elReplace: @el - model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes } - screen: 'edit' - handlersConfig: handlers - filter: @formMeta.filter - formMeta: @formMeta - params: defaults - isDisabled: !@ticket.editable() - taskKey: @taskKey - core_workflow: { - callbacks: [@markForm] - } - #bookmarkable: true - ) - else - @controllerFormSidebarTicket = new App.ControllerForm( - elReplace: @el - model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes } - screen: 'edit' - handlersConfig: handlers - filter: @formMeta.filter - formMeta: @formMeta - params: defaults - isDisabled: @ticket.editable() - taskKey: @taskKey - core_workflow: { - callbacks: [@markForm] - } - #bookmarkable: true - ) + editable = @ticket.editable() + if followUpPossible == 'new_ticket' && ticketState != 'closed' || followUpPossible != 'new_ticket' || @permissionCheck('admin') || @ticket.currentView() is 'agent' + editable = !editable + # reset updated_at for the sidbar because we render a new state + # it is used to compare the ticket with the rendered data later + # and needed to prevent race conditions + @el.removeAttr('data-ticket-updated-at') + + @controllerFormSidebarTicket = new App.ControllerForm( + elReplace: @el + model: { className: 'Ticket', configure_attributes: @formMeta.configure_attributes || App.Ticket.configure_attributes } + screen: 'edit' + handlersConfig: handlers + filter: @formMeta.filter + formMeta: @formMeta + params: defaults + isDisabled: editable + taskKey: @taskKey + core_workflow: { + callbacks: [@markForm] + } + #bookmarkable: true + ) + + # set updated_at for the sidbar because we render a new state + @el.attr('data-ticket-updated-at', defaults.updated_at) @markForm(true) return if @resetBind diff --git a/app/assets/javascripts/app/controllers/widget/organization.coffee b/app/assets/javascripts/app/controllers/widget/organization.coffee index dc90b7611..574490cde 100644 --- a/app/assets/javascripts/app/controllers/widget/organization.coffee +++ b/app/assets/javascripts/app/controllers/widget/organization.coffee @@ -33,7 +33,7 @@ class App.WidgetOrganization extends App.Controller elLocal.find('.js-userList').html(members) ) - if @organization.member_ids.length < @memberLimit + if @organization.member_ids.length <= @memberLimit @el.find('.js-showMoreMembers').parent().addClass('hidden') else @el.find('.js-showMoreMembers').parent().removeClass('hidden') diff --git a/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee b/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee index ec51e6ca0..fa1cf4f2c 100644 --- a/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee +++ b/app/assets/javascripts/app/lib/app_post/_object_organization_autocompletion.coffee @@ -14,6 +14,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller 'click': 'stopPropagation' 'change .js-objectId': 'executeCallback' 'click .js-remove': 'removeThisToken' + 'click .js-showMoreMembers': 'showMoreMembers' elements: '.recipientList': 'recipientList' @@ -251,14 +252,42 @@ class App.ObjectOrganizationAutocompletion extends App.Controller objectCount: objectCount ) + showMoreMembers: (e) -> + @preventDefaultAndStopPropagation(e) + + memberElement = $(e.target).closest('.js-showMoreMembers') + oldMemberLimit = memberElement.attr('organization-member-limit') + newMemberLimit = (parseInt(oldMemberLimit / 25) + 1) * 25 + memberElement.attr('organization-member-limit', newMemberLimit) + + @renderMembers(memberElement, oldMemberLimit, newMemberLimit) + + renderMembers: (element, fromMemberLimit, toMemberLimit) -> + id = element.closest('.recipientList-organizationMembers').attr('organization-id') + organization = App.Organization.find(id) + + # only first 10 members else we would need more ajax requests + organization.members(fromMemberLimit, toMemberLimit, (users) => + for user in users + element.before(@buildObjectItem(user)) + + if element.closest('ul').hasClass('is-shown') + @showOrganizationMembers(undefined, element.closest('ul')) + ) + + if organization.member_ids.length <= toMemberLimit + element.addClass('hidden') + else + element.removeClass('hidden') + buildOrganizationMembers: (organization) => - organizationMemebers = $( App.view(@templateOrganizationItemMembers)( + organizationMembers = $( App.view(@templateOrganizationItemMembers)( organization: organization ) ) - if organization[@referenceAttribute] - for objectId in organization[@referenceAttribute] - object = App[@objectSingle].fullLocal(objectId) - organizationMemebers.append(@buildObjectItem(object)) + + @renderMembers(organizationMembers.find('.js-showMoreMembers'), 0, 10) + + organizationMembers buildObjectItem: (object) => icon = @objectIcon @@ -404,8 +433,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller e.stopPropagation() listEntry = $(e.currentTarget) - organizationId = listEntry.data('organization-id') - + organizationId = listEntry.data('organization-id') || listEntry.attr('organization-id') @organizationList = @$("[organization-id=#{ organizationId }]") return if !@organizationList.get(0) diff --git a/app/assets/javascripts/app/lib/app_post/ajax.coffee b/app/assets/javascripts/app/lib/app_post/ajax.coffee index 9ee86e2f6..a0399c21d 100644 --- a/app/assets/javascripts/app/lib/app_post/ajax.coffee +++ b/app/assets/javascripts/app/lib/app_post/ajax.coffee @@ -102,12 +102,18 @@ class _ajaxSingleton # do not show any error message with code 502 return if status is 502 + try + json = JSON.parse(detail) + text = json.error_human || json.error + + text = detail if !text + + escaped = App.Utils.htmlEscape(text) + # show error message - new App.ControllerModal( - head: "StatusCode: #{status}" - contentInline: "
#{App.Utils.htmlEscape(detail)}
" - buttonClose: true - buttonSubmit: false + new App.ControllerTechnicalErrorModal( + contentCode: escaped + head: "StatusCode: #{status}" ) ) diff --git a/app/assets/javascripts/app/lib/app_post/full_quote_header.coffee b/app/assets/javascripts/app/lib/app_post/full_quote_header.coffee index 63b7a7761..fc21173f4 100644 --- a/app/assets/javascripts/app/lib/app_post/full_quote_header.coffee +++ b/app/assets/javascripts/app/lib/app_post/full_quote_header.coffee @@ -28,7 +28,7 @@ class App.FullQuoteHeader @fullQuoteHeaderForwardTo: (article) -> if article.type.name is 'email' || article.type.name is 'web' - @fullQuoteHeaderEnsurePrivacy(article.to) || article.to + @fullQuoteHeaderEnsureMultiPrivacy(article.to) else if article.sender.name is 'Customer' && article.type.name is 'phone' if email_address_id = App.Group.findByAttribute('name', article.to)?.email_address_id App.EmailAddress.find(email_address_id).displayName() @@ -36,15 +36,17 @@ class App.FullQuoteHeader article.to else if article.sender.name is 'Agent' && article.type.name is 'phone' ticket = App.Ticket.find article.ticket_id - @fullQuoteHeaderEnsurePrivacy(ticket.customer_id) || @fullQuoteHeaderEnsurePrivacy(article.to) || article.to + @fullQuoteHeaderEnsurePrivacy(ticket.customer_id) || @fullQuoteHeaderEnsureMultiPrivacy(article.to) else article.to @fullQuoteHeaderForwardCC: (article) -> - return if !article.cc + @fullQuoteHeaderEnsureMultiPrivacy(article.cc) - article - .cc + @fullQuoteHeaderEnsureMultiPrivacy: (input) -> + return if !input + + input .split(',') .map (elem) -> elem.trim() diff --git a/app/assets/javascripts/app/lib/app_post/i18n.coffee b/app/assets/javascripts/app/lib/app_post/i18n.coffee index 61dcb8036..86482bedb 100644 --- a/app/assets/javascripts/app/lib/app_post/i18n.coffee +++ b/app/assets/javascripts/app/lib/app_post/i18n.coffee @@ -8,7 +8,12 @@ class App.i18n @translateDeep: (input, args...) -> if _instance == undefined _instance ?= new _i18nSingleton() - _instance.translateDeep(input, args) + _instance.translateDeep(input, args, false) + + @translateDeepPlain: (input, args...) -> + if _instance == undefined + _instance ?= new _i18nSingleton() + _instance.translateDeep(input, args, true) @translateContent: (string, args...) -> if _instance == undefined @@ -230,17 +235,20 @@ class _i18nSingleton extends Spine.Module return string if !string @translate(string, args, true) - translateDeep: (input, args) => + translateDeep: (input, args, plain) => if _.isArray(input) _.map input, (item) => - @translateDeep(item, args) + @translateDeep(item, args, plain) else if _.isObject(input) _.reduce _.keys(input), (memo, item) => - memo[item] = @translateDeep(input[item]) + memo[item] = @translateDeep(input[item], args, plain) memo , {} else - @translateInline(input, args) + if plain + @translatePlain(input, args) + else + @translateInline(input, args) translateContent: (string, args) => diff --git a/app/assets/javascripts/app/lib/app_post/ticket_create_collection.coffee b/app/assets/javascripts/app/lib/app_post/ticket_create_collection.coffee deleted file mode 100644 index a4ff9e0bd..000000000 --- a/app/assets/javascripts/app/lib/app_post/ticket_create_collection.coffee +++ /dev/null @@ -1,27 +0,0 @@ -class _Singleton extends App._CollectionSingletonBase - event: 'ticket_create_attributes' - restEndpoint: '/ticket_create' - -class App.TicketCreateCollection - _instance = new _Singleton - - @get: -> - _instance.get() - - @one: (callback, init = true) -> - _instance.bind(callback, init, true) - - @bind: (callback, init = true) -> - _instance.bind(callback, init, false) - - @unbind: (callback) -> - _instance.unbind(callback) - - @unbindById: (id) -> - _instance.unbindById(id) - - @trigger: -> - _instance.trigger() - - @fetch: -> - _instance.fetch() diff --git a/app/assets/javascripts/app/lib/app_post/utils.coffee b/app/assets/javascripts/app/lib/app_post/utils.coffee index cbc0585d2..04a7fbac4 100644 --- a/app/assets/javascripts/app/lib/app_post/utils.coffee +++ b/app/assets/javascripts/app/lib/app_post/utils.coffee @@ -1327,7 +1327,7 @@ class App.Utils if type is 'email' && !e.attrs.value.match(/@/) || e.attrs.value.match(/\s/) e.preventDefault() return false - e.attrs.label = e.attrs.value + e.attrs.label ||= e.attrs.value true ) App.Delay.set(a, 500, undefined, 'tags') diff --git a/app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js b/app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js index 08339f039..73d95b996 100644 --- a/app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js +++ b/app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js @@ -654,6 +654,11 @@ var tokensBefore = this.getTokensList() this.setTokens( this.$input.val(), true ) + // remove token text was cleared while editing + if (this.$input.data( 'edit' ) && !this.$input.val()) { + this.$element.val( this.getTokensList() ) + } + if (tokensBefore == this.getTokensList() && this.$input.val().length) return false // No tokens were added, do nothing (prevent form submit) diff --git a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js index 940073406..31ef2c486 100644 --- a/app/assets/javascripts/app/lib/base/jquery.contenteditable.js +++ b/app/assets/javascripts/app/lib/base/jquery.contenteditable.js @@ -114,11 +114,22 @@ sel = window.getSelection() if (sel) { node = $(sel.anchorNode) - if (node && node.parent() && node.parent().is('blockquote')) { - e.preventDefault() - document.execCommand('Insertparagraph') - document.execCommand('Outdent') - return + + if (node.closest('blockquote').length > 0) { + + // Special handling when the line is not wrapped inside of a html element. + if (!node.is('div') && node.parent().is('blockquote') && node.text()) { + e.preventDefault() + document.execCommand('formatBlock', false, 'div') + document.execCommand('insertParagraph') + return + } + if (!e.shiftKey && node && (node.is('blockquote') || (node.parent() && node.parent().is('blockquote')) || !node.text())) { + e.preventDefault() + document.execCommand('insertParagraph') + document.execCommand('outdent') + return + } } } diff --git a/app/assets/javascripts/app/lib/mixins/view_helpers.coffee b/app/assets/javascripts/app/lib/mixins/view_helpers.coffee index 64686fd3c..fd4a0db12 100644 --- a/app/assets/javascripts/app/lib/mixins/view_helpers.coffee +++ b/app/assets/javascripts/app/lib/mixins/view_helpers.coffee @@ -41,17 +41,33 @@ App.ViewHelpers = return '' if isNaN(parseInt(time)) # Hours, minutes and seconds - hrs = ~~parseInt((time / 3600)) + hrs = ~~parseInt((time / 3600)) mins = ~~parseInt(((time % 3600) / 60)) secs = parseInt(time % 60) # Output like "1:01" or "4:03:59" or "123:03:59" mins = "0#{mins}" if mins < 10 secs = "0#{secs}" if secs < 10 + if hrs > 0 return "#{hrs}:#{mins}:#{secs}" + "#{mins}:#{secs}" + # define time_duration / hh:mm + time_duration_hh_mm: (time_in_minutes) -> + return '' if !time_in_minutes + return '' if isNaN(parseInt(time_in_minutes)) + + # Hours, minutes and seconds + hrs = ~~parseInt((time_in_minutes / 60)) + mins = ~~parseInt((time_in_minutes % 60)) + + hrs = "0#{hrs}" if hrs < 10 + mins = "0#{mins}" if mins < 10 + + "#{hrs}:#{mins}" + # define mask helper # mask an value like 'a***********yz' M: (item, start = 1, end = 2) -> diff --git a/app/assets/javascripts/app/lib/spine/ajax.coffee b/app/assets/javascripts/app/lib/spine/ajax.coffee index f04409b71..491bea7ea 100644 --- a/app/assets/javascripts/app/lib/spine/ajax.coffee +++ b/app/assets/javascripts/app/lib/spine/ajax.coffee @@ -234,12 +234,11 @@ class Singleton extends Base failResponse: (options) => (xhr, statusText, error, settings) => - if options.failResponseNoTrigger isnt true - switch settings.type - when 'POST' then @createFailed() - when 'DELETE' then @destroyFailed() - # add errors to calllback - @record.trigger('ajaxError', @record, xhr, statusText, error, settings) + switch settings.type + when 'POST' then @createFailed() + when 'DELETE' then @destroyFailed() + # add errors to calllback + @record.trigger('ajaxError', @record, xhr, statusText, error, settings) #options.fail?.call(@record, settings) detailsRaw = xhr.responseText @@ -247,8 +246,7 @@ class Singleton extends Base details = JSON.parse(detailsRaw) options.fail?.call(@record, settings, details) - if options.failResponseNoTrigger isnt true - @record.trigger('destroy', @record) + @record.trigger('destroy', @record) # /add errors to calllback createFailed: -> diff --git a/app/assets/javascripts/app/models/_application_model.coffee b/app/assets/javascripts/app/models/_application_model.coffee index 78782a842..455bb381d 100644 --- a/app/assets/javascripts/app/models/_application_model.coffee +++ b/app/assets/javascripts/app/models/_application_model.coffee @@ -916,20 +916,25 @@ set new attributes of model (remove already available attributes) # use jquery instead of ._clone() because we need a deep copy of the obj @org_configure_attributes = $.extend(true, [], @configure_attributes) + configure_attributes = $.extend(true, [], @configure_attributes) + allAttributes = [] for attribute in attributes @attributes.push attribute.name found = false - for attribute_model, index in @configure_attributes + for attribute_model, index in configure_attributes continue if attribute_model.name != attribute.name - @configure_attributes[index] = _.extend(attribute_model, attribute) + allAttributes.push $.extend(true, attribute_model, attribute) + configure_attributes.splice(index, 1) # remove found attribute found = true break if !found - @configure_attributes.push attribute + allAttributes.push $.extend(true, {}, attribute) + + @configure_attributes = $.extend(true, [], allAttributes.concat(configure_attributes)) @resetAttributes: -> return if _.isEmpty(@org_configure_attributes) diff --git a/app/assets/javascripts/app/models/object_manager_attribute.coffee b/app/assets/javascripts/app/models/object_manager_attribute.coffee index 3857e9472..6346d5714 100644 --- a/app/assets/javascripts/app/models/object_manager_attribute.coffee +++ b/app/assets/javascripts/app/models/object_manager_attribute.coffee @@ -6,8 +6,8 @@ class App.ObjectManagerAttribute extends App.Model { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, { name: 'display', display: 'Display', tag: 'input', type: 'text', limit: 100, null: false }, { name: 'object', display: 'Object', tag: 'input', readonly: 1 }, - { name: 'position', display: 'Position', tag: 'input', readonly: 1 }, { name: 'active', display: 'Active', tag: 'active', default: true }, { name: 'data_type', display: 'Format', tag: 'object_manager_attribute', null: false }, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, + { name: 'position', display: 'Position', tag: 'integer', type: 'number', limit: 100, null: true }, ] diff --git a/app/assets/javascripts/app/models/organization.coffee b/app/assets/javascripts/app/models/organization.coffee index 2189289f7..867189ba2 100644 --- a/app/assets/javascripts/app/models/organization.coffee +++ b/app/assets/javascripts/app/models/organization.coffee @@ -36,7 +36,7 @@ Using **Organisations** you can **group** customers. This has among others two i userResult = -> users = [] for user_id in member_ids - user = App.User.find(user_id) + user = App.User.fullLocal(user_id) continue if !user users.push(user) return users diff --git a/app/assets/javascripts/app/models/sla.coffee b/app/assets/javascripts/app/models/sla.coffee index 824db43cf..5724ada1f 100644 --- a/app/assets/javascripts/app/models/sla.coffee +++ b/app/assets/javascripts/app/models/sla.coffee @@ -1,5 +1,5 @@ class App.Sla extends App.Model - @configure 'Sla', 'name', 'first_response_time', 'update_time', 'solution_time', 'condition', 'calendar_id' + @configure 'Sla', 'name', 'first_response_time', 'response_time', 'update_time', 'solution_time', 'condition', 'calendar_id' @extend Spine.Model.Ajax @url: @apiPath + '/slas' @configure_attributes = [ @@ -12,6 +12,7 @@ class App.Sla extends App.Model { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, { name: 'first_response_time',skipRendering: true }, + { name: 'response_time', skipRendering: true }, { name: 'update_time', skipRendering: true }, { name: 'solution_time', skipRendering: true }, ] diff --git a/app/assets/javascripts/app/views/data_privacy/preview.jst.eco b/app/assets/javascripts/app/views/data_privacy/preview.jst.eco index 02856323a..90a19c95d 100644 --- a/app/assets/javascripts/app/views/data_privacy/preview.jst.eco +++ b/app/assets/javascripts/app/views/data_privacy/preview.jst.eco @@ -14,7 +14,7 @@

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

-

<%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translateInline('delete').toUpperCase()) %>

+

<%- @T('There is no rollback of this deletion possible. If you are absolutely sure to do this, then type in "%s" into the input.', App.i18n.translatePlain('delete').toUpperCase()) %>

<%- @sure_html %>
diff --git a/app/assets/javascripts/app/views/generic/auth_provider.jst.eco b/app/assets/javascripts/app/views/generic/auth_provider.jst.eco new file mode 100644 index 000000000..0447f0ce6 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/auth_provider.jst.eco @@ -0,0 +1 @@ + diff --git a/app/assets/javascripts/app/views/generic/business_hours.jst.eco b/app/assets/javascripts/app/views/generic/business_hours.jst.eco index dfb097935..1ae7ffe9b 100644 --- a/app/assets/javascripts/app/views/generic/business_hours.jst.eco +++ b/app/assets/javascripts/app/views/generic/business_hours.jst.eco @@ -20,8 +20,8 @@ - - + + <% else: %> <% end %> @@ -45,4 +45,4 @@ <%- @Icon('plus-small') %> <% end %> - \ No newline at end of file + diff --git a/app/assets/javascripts/app/views/generic/object_search/input.jst.eco b/app/assets/javascripts/app/views/generic/object_search/input.jst.eco index a56584e7e..19f6fe0a4 100644 --- a/app/assets/javascripts/app/views/generic/object_search/input.jst.eco +++ b/app/assets/javascripts/app/views/generic/object_search/input.jst.eco @@ -3,10 +3,10 @@ <% if @attribute.multiple: %> <%- @tokens %> <% end %> - role="textbox" aria-autocomplete="list" value="<%= @name %>" aria-haspopup="true"> + role="textbox" aria-autocomplete="list" value="<%= @name %>" aria-haspopup="true"> <% if @attribute.disableCreateObject isnt true: %><%- @Icon('arrow-down', 'dropdown-arrow') %><% end %> \ No newline at end of file + diff --git a/app/assets/javascripts/app/views/generic/object_search/item_organization_members.jst.eco b/app/assets/javascripts/app/views/generic/object_search/item_organization_members.jst.eco index eb5c080d6..3af3aba7c 100644 --- a/app/assets/javascripts/app/views/generic/object_search/item_organization_members.jst.eco +++ b/app/assets/javascripts/app/views/generic/object_search/item_organization_members.jst.eco @@ -4,4 +4,9 @@ <%- @Icon('arrow-left') %> <%- @T('Back') %> - \ No newline at end of file + +
  • +
    + <%- @T('show more') %> +
    + diff --git a/app/assets/javascripts/app/views/generic/sla_times.jst.eco b/app/assets/javascripts/app/views/generic/sla_times.jst.eco index 98fe6ea21..0c4fcda89 100644 --- a/app/assets/javascripts/app/views/generic/sla_times.jst.eco +++ b/app/assets/javascripts/app/views/generic/sla_times.jst.eco @@ -6,7 +6,7 @@ <%- @T('Time') %> <%- @T('in hours') %> - +
  • "),T.remove(),l=n}else l=0;for(v=0,u=(_=e.querySelectorAll("[style]")).length;v/g,">").replace(/"/g,""")}),function(){(function(){t.push('
    \n
    \n
    \n \n \n \n \n \n
    \n
    \n
    \n
    \n \n '),t.push(this.T(this.title)),t.push('\n
    \n
    \n
    \n \n
    \n
    \n
    \n \n
    \n
    ")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
    \n '),this.agent?(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent))):(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay))),t.push("\n "),t.push('\n
    \n
    "),t.push(this.T("Start new conversation")),t.push("
    \n
    ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('\n \n \n \n\n'),t.push(this.T("Connecting")),t.push("")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?o(e):""},s=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
    \n "),t.push(this.message),t.push("\n
    ")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
    \n
    \n '),t.push(this.status),t.push("\n
    \n
    ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?o(e):""},s=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
    '),t.push(n(this.label)),t.push(" "),t.push(n(this.time)),t.push("
    ")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
    \n \n \n \n \n \n \n \n
    ')}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
    \n \n \n \n \n \n '),t.push(this.T("All colleagues are busy.")),t.push("
    \n "),t.push(this.T("You are on waiting list position %s.",this.position)),t.push("\n
    ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
    \n '),t.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),t.push('\n
    \n
    "),t.push(this.T("Start new conversation")),t.push("
    \n
    ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")}; \ No newline at end of file +window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?o(e):""},s=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(t.push('\n\n')),t.push('\n\n '),t.push(n(this.agent.name)),t.push("\n")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")},function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,function(){"use strict";var s=Object.hasOwnProperty,i=Object.setPrototypeOf,a=Object.isFrozen,o=Object.getPrototypeOf,r=Object.getOwnPropertyDescriptor,Me=Object.freeze,e=Object.seal,l=Object.create,t="undefined"!=typeof Reflect&&Reflect,c=t.apply,d=t.construct;c||(c=function(e,t,n){return e.apply(t,n)}),Me||(Me=function(e){return e}),e||(e=function(e){return e}),d||(d=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t/gm),rt=e(/^data-[\-\w.\u00B7-\uFFFF]/),lt=e(/^aria-[\-\w]+$/),ct=e(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),dt=e(/^(?:\w+script|data):/i),ut=e(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ht="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function mt(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t/i,n))ze(l,e);else{V&&(n=Pe(n,_," "),n=Pe(n,D," "));var d=e.nodeName.toLowerCase();if(Ie(d,s,n))try{c?e.setAttributeNS(c,l,n):e.setAttribute(l,n),je(u.removed)}catch(e){}}}Oe("afterSanitizeAttributes",e,null)}},De=function e(t){var n=void 0,s=Le(t);for(Oe("beforeSanitizeShadowDOM",t,null);n=s.nextNode();)Oe("uponSanitizeShadowNode",n,null),Ee(n)||(n.content instanceof h&&e(n.content),_e(n));Oe("afterSanitizeShadowDOM",t,null)};return u.sanitize=function(e,t){var n=void 0,s=void 0,o=void 0,i=void 0,a=void 0;if((fe=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!xe(e)){if("function"!=typeof e.toString)throw Ue("toString is not a function");if("string"!=typeof(e=e.toString()))throw Ue("dirty is not a string, aborting")}if(!u.isSupported){if("object"===ht(c.toStaticHTML)||"function"==typeof c.toStaticHTML){if("string"==typeof e)return c.toStaticHTML(e);if(xe(e))return c.toStaticHTML(e.outerHTML)}return e}if(Z||be(t),u.removed=[],"string"==typeof e&&(oe=!1),oe);else if(e instanceof m)1===(s=(n=Ae("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===s.nodeName||"HTML"===s.nodeName?n=s:n.appendChild(s);else{if(!$&&!V&&!G&&-1===e.indexOf("<"))return T&&te?T.createHTML(e):e;if(!(n=Ae(e)))return $?null:C}n&&X&&ke(n.firstChild);for(var r=Le(oe?e:n);o=r.nextNode();)3===o.nodeType&&o===i||Ee(o)||(o.content instanceof h&&De(o.content),_e(o),i=o);if(i=null,oe)return e;if($){if(J)for(a=L.call(n.ownerDocument);n.firstChild;)a.appendChild(n.firstChild);else a=n;return ee&&(a=O.call(d,a,!0)),a}var l=G?n.outerHTML:n.innerHTML;return V&&(l=Pe(l,_," "),l=Pe(l,D," ")),T&&te?T.createHTML(l):l},u.setConfig=function(e){be(e),Z=!0},u.clearConfig=function(){ye=null,Z=!1},u.isValidAttribute=function(e,t,n){ye||be({});var s=Ne(e),o=Ne(t);return Ie(s,o,n)},u.addHook=function(e,t){"function"==typeof t&&(I[e]=I[e]||[],He(I[e],t))},u.removeHook=function(e){I[e]&&je(I[e])},u.removeHooks=function(e){I[e]&&(I[e]=[])},u.removeAllHooks=function(){I={}},u}()});var extend=function(e,t){for(var n in t)hasProp.call(t,n)&&(e[n]=t[n]);function s(){this.constructor=e}return s.prototype=t.prototype,e.prototype=new s,e.__super__=t.prototype,e},hasProp={}.hasOwnProperty,bind=function(e,t){return function(){return e.apply(t,arguments)}},slice=[].slice;!function(O){var s,n,o,i,a,e,r,l,c,d;r=(d=document.getElementsByTagName("script"))[d.length-1],c=O.location.protocol.replace(":",""),r&&r.src&&(l=r.src.match(".*://([^:/]*).*")[1],c=r.src.match("(.*)://[^:/]*.*")[1]),n=function(){function e(e){var t,n,s;for(t in this.options={},n=this.defaults)s=n[t],this.options[t]=s;for(t in e)s=e[t],this.options[t]=s}return e.prototype.defaults={debug:!1},e}(),s=function(e){function t(e){t.__super__.constructor.call(this,e),this.log=new i({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return extend(t,n),t}(),i=function(e){function t(){return this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),t.__super__.constructor.apply(this,arguments)}return extend(t,n),t.prototype.debug=function(){var e;if(e=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",e)},t.prototype.notice=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",e)},t.prototype.error=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("error",e)},t.prototype.log=function(e,t){var n,s,o,i,a;if(t.unshift("||"),t.unshift(e),t.unshift(this.options.logPrefix),console.log.apply(console,t),this.options.debug){for(a="",o=0,i=t.length;o"+a+""+n.innerHTML:void 0}},t}(),a=function(e){function t(){return this.stop=bind(this.stop,this),this.start=bind(this.start,this),t.__super__.constructor.apply(this,arguments)}return extend(t,s),t.prototype.timeoutStartedAt=null,t.prototype.logPrefix="timeout",t.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},t.prototype.start=function(){var e,t,n;return this.stop(),t=new Date,e=function(){var e;if(e=new Date-new Date(t.getTime()+1e3*n.options.timeout*60),n.log.debug("Timeout check for "+n.options.timeout+" minutes (left "+e/1e3+" sec.)"),!(e<0))return n.stop(),n.options.callback()},(n=this).log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(e,1e3*this.options.timeoutIntervallCheck*60)},t.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},t}(),o=function(e){function t(){return this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),t.__super__.constructor.apply(this,arguments)}return extend(t,s),t.prototype.logPrefix="io",t.prototype.set=function(e){var t,n,s;for(t in n=[],e)s=e[t],n.push(this.options[t]=s);return n},t.prototype.connect=function(){var t,o,n,s;return this.log.debug("Connecting to "+this.options.host),this.ws=new O.WebSocket(""+this.options.host),this.ws.onopen=(t=this,function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}),this.ws.onmessage=(o=this,function(e){var t,n,s;for(s=JSON.parse(e.data),o.log.debug("onMessage",e.data),t=0,n=s.length;tChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5,onReady:void 0,onCloseAnimationEnd:void 0,onError:void 0,onOpenAnimationEnd:void 0,onConnectionReestablished:void 0,onSessionClosed:void 0,onConnectionEstablished:void 0,onCssLoaded:void 0},n.prototype.logPrefix="chat",n.prototype._messageCount=0,n.prototype.isOpen=!1,n.prototype.blinkOnlineInterval=null,n.prototype.stopBlinOnlineStateTimeout=null,n.prototype.showTimeEveryXMinutes=2,n.prototype.lastTimestamp=null,n.prototype.lastAddedType=null,n.prototype.inputDisabled=!1,n.prototype.inputTimeout=null,n.prototype.isTyping=!1,n.prototype.state="offline",n.prototype.initialQueueDelay=1e4,n.prototype.translations={da:{"Chat with us!":"Chat med os!","Scroll down to see new messages":"Scroll ned for at se nye beskeder",Online:"Online",Offline:"Offline",Connecting:"Forbinder","Connection re-established":"Forbindelse genoprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat lukket af %s","Compose your message...":"Skriv en besked...","All colleagues are busy.":"Alle kollegaer er optaget.","You are on waiting list position %s.":"Du er i venteliste som nummer %s.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da du ikke har svaret i de sidste %s minutter er din samtale med %s blevet lukket.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da du ikke har svaret i de sidste %s minutter er din samtale blevet lukket.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, det tager længere end forventet at få en ledig plads. Prøv venligst igen senere eller send os en e-mail. På forhånd tak!"},de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Chat closed by %s":"Chat beendet von %s","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Chat closed by %s":"Chat cerrado por %s","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fi:{"Chat with us!":"Keskustele kanssamme!","Scroll down to see new messages":"Rullaa alas nähdäksesi uudet viestit",Online:"Paikalla",Offline:"Poissa",Connecting:"Yhdistetään","Connection re-established":"Yhteys muodostettu uudelleen",Today:"Tänään",Send:"Lähetä","Chat closed by %s":"%s sulki keskustelun","Compose your message...":"Luo viestisi...","All colleagues are busy.":"Kaikki kollegat ovat varattuja.","You are on waiting list position %s.":"Olet odotuslistalla sijalla %s.","Start new conversation":"Aloita uusi keskustelu","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Koska et vastannut viimeiseen %s minuuttiin, keskustelusi %s kanssa suljettiin.","Since you didn't respond in the last %s minutes your conversation got closed.":"Koska et vastannut viimeiseen %s minuuttiin, keskustelusi suljettiin.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Olemme pahoillamme, tyhjän paikan vapautumisessa kestää odotettua pidempään. Ole hyvä ja yritä myöhemmin uudestaan tai lähetä meille sähköpostia. Kiitos!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Chat closed by %s":"Chat fermé par %s","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collaborateurs sont occupés actuellement.","You are on waiting list position %s.":"Vous êtes actuellement en position %s dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s sera fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Nous vous remercions!"},he:{"Chat with us!":"שוחחאיתנו!","Scroll down to see new messages":"גלול מטה כדי לראות הודעות חדשות",Online:"מחובר",Offline:"מנותק",Connecting:"מתחבר","Connection re-established":"החיבור שוחזר",Today:"היום",Send:"שלח","Chat closed by %s":'הצאט נסגר ע"י %s',"Compose your message...":"כתוב את ההודעה שלך ...","All colleagues are busy.":"כל הנציגים תפוסים","You are on waiting list position %s.":"מיקומך בתור %s.","Start new conversation":"התחל שיחה חדשה","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"מכיוון שלא הגבת במהלך %s דקות השיחה שלך עם %s נסגרה.","Since you didn't respond in the last %s minutes your conversation got closed.":"מכיוון שלא הגבת במהלך %s הדקות האחרונות השיחה שלך נסגרה.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":'מצטערים, הזמן לקבלת נציג ארוך מהרגיל. נסה שוב מאוחר יותר או שלח לנו דוא"ל. תודה!'},hu:{"Chat with us!":"Chatelj velünk!","Scroll down to see new messages":"Görgess lejjebb az újabb üzenetekért",Online:"Online",Offline:"Offline",Connecting:"Csatlakozás","Connection re-established":"Újracsatlakozás",Today:"Ma",Send:"Küldés","Chat closed by %s":"A beszélgetést lezárta %s","Compose your message...":"Írj üzenetet...","All colleagues are busy.":"Jelenleg minden kollégánk elfoglalt.","You are on waiting list position %s.":"A várólistán a %s. pozícióban várakozol.","Start new conversation":"Új beszélgetés indítása","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Mivel %s perce nem érkezett újabb üzenet, ezért a %s kollégával folytatott beszéletést lezártuk.","Since you didn't respond in the last %s minutes your conversation got closed.":"Mivel %s perce nem érkezett válasz, a beszélgetés lezárult.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Sajnáljuk, de a várakozási idő hosszabb a szokásosnál. Kérlek próbáld újra, vagy írd meg kérdésed emailben. Köszönjük!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Chat closed by %s":"Chat gesloten door %s","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},it:{"Chat with us!":"Chatta con noi!","Scroll down to see new messages":"Scorrere verso il basso per vedere i nuovi messaggi",Online:"Online",Offline:"Offline",Connecting:"Collegamento","Connection re-established":"Collegamento ristabilito",Today:"Oggi",Send:"Invio","Chat closed by %s":"Conversazione chiusa da %s","Compose your message...":"Comporre il tuo messaggio...","All colleagues are busy.":"Tutti i colleghi sono occupati.","You are on waiting list position %s.":"Siete in posizione lista d' attesa %s.","Start new conversation":"Avviare una nuova conversazione","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione con %s si è chiusa.","Since you didn't respond in the last %s minutes your conversation got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua conversazione si è chiusa.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Ci dispiace, ci vuole più tempo come previsto per ottenere uno slot vuoto. Per favore riprova più tardi o inviaci un' e-mail. Grazie!"},pl:{"Chat with us!":"Czatuj z nami!","Scroll down to see new messages":"Przewiń w dół, aby wyświetlić nowe wiadomości",Online:"Online",Offline:"Offline",Connecting:"Łączenie","Connection re-established":"Ponowne nawiązanie połączenia",Today:"dzisiejszy",Send:"Wyślij","Chat closed by %s":"Czat zamknięty przez %s","Compose your message...":"Utwórz swoją wiadomość...","All colleagues are busy.":"Wszyscy koledzy są zajęci.","You are on waiting list position %s.":"Na liście oczekujących znajduje się pozycja %s.","Start new conversation":"Rozpoczęcie nowej konwersacji","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!"},"pt-br":{"Chat with us!":"Chat fale conosco!","Scroll down to see new messages":"Role para baixo, para ver nosvas mensagens",Online:"Online",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexão restabelecida",Today:"Hoje",Send:"Enviar","Chat closed by %s":"Chat encerrado por %s","Compose your message...":"Escreva sua mensagem...","All colleagues are busy.":"Todos os agentes estão ocupados.","You are on waiting list position %s.":"Você está na posição %s na fila de espera.","Start new conversation":"Iniciar uma nova conversa","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Como você não respondeu nos últimos %s minutos sua conversa com %s foi encerrada.","Since you didn't respond in the last %s minutes your conversation got closed.":"Como você não respondeu nos últimos %s minutos sua conversa foi encerrada.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Desculpe, mas o tempo de espera por um agente foi excedido. Tente novamente mais tarde ou nós envie um email. Obrigado"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"},ru:{"Chat with us!":"Напишите нам!","Scroll down to see new messages":"Прокрутите, чтобы увидеть новые сообщения",Online:"Онлайн",Offline:"Оффлайн",Connecting:"Подключение","Connection re-established":"Подключение восстановлено",Today:"Сегодня",Send:"Отправить","Chat closed by %s":"%s закрыл чат","Compose your message...":"Напишите сообщение...","All colleagues are busy.":"Все сотрудники заняты","You are on waiting list position %s.":"Вы в списке ожидания под номером %s","Start new conversation":"Начать новую переписку.","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.","Since you didn't respond in the last %s minutes your conversation got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!"},sv:{"Chat with us!":"Chatta med oss!","Scroll down to see new messages":"Rulla ner för att se nya meddelanden",Online:"Online",Offline:"Offline",Connecting:"Ansluter","Connection re-established":"Anslutningen återupprättas",Today:"I dag",Send:"Skicka","Chat closed by %s":"Chatt stängd av %s","Compose your message...":"Skriv ditt meddelande...","All colleagues are busy.":"Alla kollegor är upptagna.","You are on waiting list position %s.":"Du är på väntelistan som position %s.","Start new conversation":"Starta ny konversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Eftersom du inte svarat inom %s minuterna i din konversation med %s så stängdes chatten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Då du inte svarat inom de senaste %s minuterna så avslutades din chatt.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi är ledsna, det tar längre tid som förväntat att få en ledig plats. Försök igen senare eller skicka ett e-postmeddelande till oss. Tack!"},no:{"Chat with us!":"Chat med oss!","Scroll down to see new messages":"Bla ned for å se nye meldinger",Online:"Pålogget",Offline:"Avlogget",Connecting:"Koble til","Connection re-established":"Tilkoblingen er gjenopprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat avsluttes om %s","Compose your message...":"Skriv din melding...","All colleagues are busy.":"Alle våre kolleger er for øyeblikket opptatt.","You are on waiting list position %s.":"Du står nå i kø og er nr. %s på ventelisten.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med %s nå avsluttes.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!"},nb:{"Chat with us!":"Chat med oss!","Scroll down to see new messages":"Bla ned for å se nye meldinger",Online:"Pålogget",Offline:"Avlogget",Connecting:"Koble til","Connection re-established":"Tilkoblingen er gjenopprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat avsluttes om %s","Compose your message...":"Skriv din melding...","All colleagues are busy.":"Alle våre kolleger er for øyeblikket opptatt.","You are on waiting list position %s.":"Du står nå i kø og er nr. %s på ventelisten.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med %s nå avsluttes.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!"},el:{"Chat with us!":"Επικοινωνήστε μαζί μας!","Scroll down to see new messages":"Μεταβείτε κάτω για να δείτε τα νέα μηνύματα",Online:"Σε σύνδεση",Offline:"Αποσυνδεμένος",Connecting:"Σύνδεση","Connection re-established":"Η σύνδεση αποκαταστάθηκε",Today:"Σήμερα",Send:"Αποστολή","Chat closed by %s":"Η συνομιλία έκλεισε από τον/την %s","Compose your message...":"Γράψτε το μήνυμα σας...","All colleagues are busy.":"Όλοι οι συνάδελφοι μας είναι απασχολημένοι.","You are on waiting list position %s.":"Βρίσκεστε σε λίστα αναμονής στη θέση %s.","Start new conversation":"Έναρξη νέας συνομιλίας","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας με τον/την %s έκλεισε.","Since you didn't respond in the last %s minutes your conversation got closed.":"Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας έκλεισε.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Λυπούμαστε που χρειάζεται περισσότερος χρόνος από τον αναμενόμενο για να βρεθεί μία κενή θέση. Παρακαλούμε δοκιμάστε ξανά αργότερα ή στείλτε μας ένα email. Ευχαριστούμε!"}},n.prototype.sessionId=void 0,n.prototype.scrolledToBottom=!0,n.prototype.scrollSnapTolerance=10,n.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},n.prototype.T=function(){var e,t,n,s,o,i;if(o=arguments[0],t=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?((i=this.translations[this.options.lang])[o]||this.log.notice("Translation needed for '"+o+"'"),o=i[o]||o):this.log.notice("Translation '"+this.options.lang+"' needed!")),t)for(n=0,s=t.length;n"+L.replace(/\n/g,"
    ")+"
    ").replace(/
    <\/div>/g,"

    ")),console.log("p",s,L),"html"===s){for(e=document.createElement("div"),A=DOMPurify.sanitize(L),this.log.debug("sanitized HTML clipboard",A),e.innerHTML=A,f=!1,o=L,z=new RegExp("<(/w|w):[A-Za-z]"),o.match(z)&&(f=!0,o=o.replace(z,"")),z=new RegExp("<(/o|o):[A-Za-z]"),o.match(z)&&(f=!0,o=o.replace(z,"")),f&&(e=this.wordFilter(e)),l=0,u=(S=e.childNodes).length;lnew Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},n.prototype.onSubmit=function(e){return e.preventDefault(),this.sendMessage()},n.prototype.sendMessage=function(){var e,t;if(e=this.input.innerHTML)return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),t=this.view("message")({message:e,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.querySelector(".zammad-chat-message--typing")?(this.lastAddedType="typing-placeholder",this.el.querySelector(".zammad-chat-message--typing").insertAdjacentHTML("beforebegin",t)):(this.lastAddedType="message--customer",this.body.insertAdjacentHTML("beforeend",t)),this.input.innerHTML="",this.scrollToBottom(),this.send("chat_session_message",{content:e,id:this._messageCount,session_id:this.sessionId})},n.prototype.receiveMessage=function(e){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:e.message.content,id:e.id,from:"agent"}),this.scrollToBottom({showHint:!0})},n.prototype.renderMessage=function(e){return this.lastAddedType="message--"+e.from,e.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.body.insertAdjacentHTML("beforeend",this.view("message")(e))},n.prototype.open=function(){var e;if(!this.isOpen)return this.isOpen=!0,this.log.debug("open widget"),this.show(),this.sessionId||this.showLoader(),this.el.classList.add("zammad-chat-is-open"),e=this.el.clientHeight-this.el.querySelector(".zammad-chat-header").offsetHeight,this.el.style.transform="translateY("+e+"px)",this.el.clientHeight,this.sessionId?(this.el.style.transform="",this.onOpenAnimationEnd()):(this.el.addEventListener("transitionend",this.onOpenAnimationEnd),this.el.classList.add("zammad-chat--animate"),this.el.clientHeight,this.el.style.transform="",this.send("chat_session_init",{url:O.location.href}));this.log.debug("widget already open, block")},n.prototype.onOpenAnimationEnd=function(){var e;return this.el.removeEventListener("transitionend",this.onOpenAnimationEnd),this.el.classList.remove("zammad-chat--animate"),this.idleTimeout.stop(),this.isFullscreen&&this.disableScrollOnRoot(),"function"==typeof(e=this.options).onOpenAnimationEnd?e.onOpenAnimationEnd():void 0},n.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},n.prototype.toggle=function(e){return this.isOpen?this.close(e):this.open(e)},n.prototype.close=function(e){var t;if(this.isOpen)return this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId&&(this.log.debug("session close before widget close"),this.sessionClose()),this.log.debug("close widget"),e&&e.stopPropagation(),this.isFullscreen&&this.enableScrollOnRoot(),t=this.el.clientHeight-this.el.querySelector(".zammad-chat-header").offsetHeight,this.el.addEventListener("transitionend",this.onCloseAnimationEnd),this.el.classList.add("zammad-chat--animate"),document.offsetHeight,this.el.style.transform="translateY("+t+"px)";this.log.debug("can't close widget, it's not open")},n.prototype.onCloseAnimationEnd=function(){var e;return this.el.removeEventListener("transitionend",this.onCloseAnimationEnd),this.el.classList.remove("zammad-chat-is-open","zammad-chat--animate"),this.el.style.transform="",this.showLoader(),this.el.querySelector(".zammad-chat-welcome").classList.remove("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent").classList.add("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent-status").classList.add("zammad-chat-is-hidden"),this.isOpen=!1,"function"==typeof(e=this.options).onCloseAnimationEnd&&e.onCloseAnimationEnd(),this.io.reconnect()},n.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.classList.remove("zammad-chat-is-shown"),this.el.classList.remove("zammad-chat-is-loaded")):void 0},n.prototype.show=function(){if("offline"!==this.state)return this.el.classList.add("zammad-chat-is-loaded"),this.el.classList.add("zammad-chat-is-shown")},n.prototype.disableInput=function(){return this.inputDisabled=!0,this.input.setAttribute("contenteditable",!1),this.el.querySelector(".zammad-chat-send").disabled=!0,this.io.close()},n.prototype.enableInput=function(){return this.inputDisabled=!1,this.input.setAttribute("contenteditable",!0),this.el.querySelector(".zammad-chat-send").disabled=!1},n.prototype.hideModal=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=""},n.prototype.onQueueScreen=function(e){var t,n;if(this.setSessionId(e.session_id),t=function(){return n.onQueue(e),n.waitingListTimeout.start()},!(n=this).initialQueueDelay||this.onInitialQueueDelayId)return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),t();this.onInitialQueueDelayId=setTimeout(t,this.initialQueueDelay)},n.prototype.onQueue=function(e){return this.log.notice("onQueue",e.position),this.inQueue=!0,this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("waiting")({position:e.position})},n.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.querySelector(".zammad-chat-message--typing")&&(this.maybeAddTimestamp(),this.body.insertAdjacentHTML("beforeend",this.view("typingIndicator")()),this.isVisible(this.el.querySelector(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},n.prototype.onAgentTypingEnd=function(){if(this.el.querySelector(".zammad-chat-message--typing"))return this.el.querySelector(".zammad-chat-message--typing").remove()},n.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},n.prototype.maybeAddTimestamp=function(){var e,t,n;if(n=Date.now(),!this.lastTimestamp||n-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return e=this.T("Today"),t=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(e,t),this.lastTimestamp=n):(this.body.insertAdjacentHTML("beforeend",this.view("timestamp")({label:e,time:t})),this.lastTimestamp=n,this.lastAddedType="timestamp",this.scrollToBottom())},n.prototype.updateLastTimestamp=function(e,t){var n;if(this.el&&(n=this.el.querySelectorAll(".zammad-chat-body .zammad-chat-timestamp")))return n[n.length-1].outerHTML=this.view("timestamp")({label:e,time:t})},n.prototype.addStatus=function(e){if(this.el)return this.maybeAddTimestamp(),this.body.insertAdjacentHTML("beforeend",this.view("status")({status:e})),this.scrollToBottom()},n.prototype.detectScrolledtoBottom=function(){var e;if(e=this.body.scrollTop+this.body.offsetHeight,this.scrolledToBottom=Math.abs(e-this.body.scrollHeight)<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.querySelector(".zammad-scroll-hint").classList.add("is-hidden")},n.prototype.showScrollHint=function(){return this.el.querySelector(".zammad-scroll-hint").classList.remove("is-hidden"),this.body.scrollTop=this.body.scrollTop+this.el.querySelector(".zammad-scroll-hint").offsetHeight},n.prototype.onScrollHintClick=function(){return this.body.scrollTo({top:this.body.scrollHeight,behavior:"smooth"})},n.prototype.scrollToBottom=function(e){var t;return t=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.body.scrollTop=this.body.scrollHeight:t?this.showScrollHint():void 0},n.prototype.destroy=function(e){var t;return null==e&&(e={}),this.log.debug("destroy widget",e),this.setAgentOnlineState("offline"),e.remove&&this.el&&(this.el.remove(),(t=document.querySelector("."+this.options.buttonClass))&&(t.classList.add(this.options.inactiveClass),t.style.display="none")),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},n.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},n.prototype.onConnectionReestablished=function(){var e;return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established")),"function"==typeof(e=this.options).onConnectionReestablished?e.onConnectionReestablished():void 0},n.prototype.onSessionClosed=function(e){var t;return this.addStatus(this.T("Chat closed by %s",e.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop(),"function"==typeof(t=this.options).onSessionClosed?t.onSessionClosed(e):void 0},n.prototype.setSessionId=function(e){return void 0===(this.sessionId=e)?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",e)},n.prototype.onConnectionEstablished=function(e){var t;return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,e.agent&&(this.agent=e.agent),e.session_id&&this.setSessionId(e.session_id),this.body.innerHTML="",this.el.querySelector(".zammad-chat-agent").innerHTML=this.view("agent")({agent:this.agent}),this.enableInput(),this.hideModal(),this.el.querySelector(".zammad-chat-welcome").classList.add("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent").classList.remove("zammad-chat-is-hidden"),this.el.querySelector(".zammad-chat-agent-status").classList.remove("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start(),"function"==typeof(t=this.options).onConnectionEstablished?t.onConnectionEstablished(e):void 0},n.prototype.showCustomerTimeout=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout}),this.el.querySelector(".js-restart").addEventListener("click",function(){return location.reload()}),this.sessionClose()},n.prototype.showWaitingListTimeout=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("waiting_list_timeout")({delay:this.options.watingListTimeout}),this.el.querySelector(".js-restart").addEventListener("click",function(){return location.reload()}),this.sessionClose()},n.prototype.showLoader=function(){return this.el.querySelector(".zammad-chat-modal").innerHTML=this.view("loader")()},n.prototype.setAgentOnlineState=function(e){var t;if(this.state=e,this.el)return t=e.charAt(0).toUpperCase()+e.slice(1),this.el.querySelector(".zammad-chat-agent-status").dataset.status=e,this.el.querySelector(".zammad-chat-agent-status").textContent=this.T(t)},n.prototype.detectHost=function(){var e;return e="ws://","https"===c&&(e="wss://"),this.options.host=""+e+l+"/ws"},n.prototype.loadCss=function(){var e,t,n;if(this.options.cssAutoload)return(n=this.options.cssUrl)||(n=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws$/i,""),n+="/assets/chat/chat.css"),this.log.debug("load css from '"+n+"'"),t="@import url('"+n+"');",(e=document.createElement("link")).onload=this.onCssLoaded,e.rel="stylesheet",e.href="data:text/css,"+escape(t),document.getElementsByTagName("head")[0].appendChild(e)},n.prototype.onCssLoaded=function(){var e;return this.cssLoaded=!0,this.socketReady&&this.onReady(),"function"==typeof(e=this.options).onCssLoaded?e.onCssLoaded():void 0},n.prototype.startTimeoutObservers=function(){var e,t,n;return this.idleTimeout=new a({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:(e=this,function(){return e.log.debug("Idle timeout reached, hide widget",new Date),e.destroy({remove:!0})})}),this.inactiveTimeout=new a({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:(t=this,function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})})}),this.waitingListTimeout=new a({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:(n=this,function(){return n.log.debug("Waiting list timeout reached, show timeout screen.",new Date),n.showWaitingListTimeout(),n.destroy({remove:!1})})})},n.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop,this.scrollRoot.style.overflow="hidden",this.scrollRoot.style.position="fixed"},n.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop=this.rootScrollOffset,this.scrollRoot.style.overflow="",this.scrollRoot.style.position=""},n.prototype.isVisible=function(e,n,s,o){var i,a,r,l,c,d,u,h,m,p;if(!(e.length<1))return p=O.innerWidth,m=O.innerHeight,o=o||"both",a=!0!==s||t.offsetWidth*t.offsetHeight,u=0<=(d=e.getBoundingClientRect()).top&&d.top/gi,"")).replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,"")).replace(/<(\/?)s>/gi,"<$1strike>")).replace(/ /gi," "),e.innerHTML=t,i=0,c=(A=e.querySelectorAll("p")).length;i",/^\s*\w+\./.test(P)&&(y=(b=/([0-9])\./.exec(P))?null!=(O=1<(N=parseInt(b[1],10)))?O:'
      ':"
        "}:"
          "),l"+T.innerHTML+""),T.remove(),l=n}else l=0;for(v=0,u=(_=e.querySelectorAll("[style]")).length;v/g,">").replace(/"/g,""")}),function(){(function(){t.push('
          \n
          \n
          \n \n \n \n \n \n
          \n
          \n
          \n
          \n \n '),t.push(this.T(this.title)),t.push('\n
          \n
          \n
          \n \n
          \n
          \n
          \n \n
          \n
          ")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
          \n '),this.agent?(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent))):(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay))),t.push("\n "),t.push('\n
          \n
          "),t.push(this.T("Start new conversation")),t.push("
          \n
          ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('\n \n \n \n\n'),t.push(this.T("Connecting")),t.push("")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?o(e):""},s=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
          \n "),t.push(this.message),t.push("\n
          ")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
          \n
          \n '),t.push(this.status),t.push("\n
          \n
          ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?o(e):""},s=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
          '),t.push(n(this.label)),t.push(" "),t.push(n(this.time)),t.push("
          ")}).call(this)}.call(e),e.safe=s,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
          \n \n \n \n \n \n \n \n
          ')}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
          \n \n \n \n \n \n '),t.push(this.T("All colleagues are busy.")),t.push("
          \n "),t.push(this.T("You are on waiting list position %s.",this.position)),t.push("\n
          ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(e){e||(e={});var t=[],n=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
          \n '),t.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),t.push('\n
          \n
          "),t.push(this.T("Start new conversation")),t.push("
          \n
          ")}).call(this)}.call(e),e.safe=n,e.escape=s,t.join("")}; \ No newline at end of file diff --git a/public/assets/chat/chat.coffee b/public/assets/chat/chat.coffee index 574d17483..d8663b54f 100644 --- a/public/assets/chat/chat.coffee +++ b/public/assets/chat/chat.coffee @@ -1108,16 +1108,14 @@ do($ = window.jQuery, window) -> return if @initDelayId clearTimeout(@initDelayId) - if !@sessionId - @log.debug 'can\'t close widget without sessionId' - return + if @sessionId + @log.debug 'session close before widget close' + @sessionClose() @log.debug 'close widget' event.stopPropagation() if event - @sessionClose() - if @isFullscreen @enableScrollOnRoot() diff --git a/public/assets/chat/chat.js b/public/assets/chat/chat.js index 389694bcd..8b78fe848 100644 --- a/public/assets/chat/chat.js +++ b/public/assets/chat/chat.js @@ -1416,15 +1416,14 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); if (this.initDelayId) { clearTimeout(this.initDelayId); } - if (!this.sessionId) { - this.log.debug('can\'t close widget without sessionId'); - return; + if (this.sessionId) { + this.log.debug('session close before widget close'); + this.sessionClose(); } this.log.debug('close widget'); if (event) { event.stopPropagation(); } - this.sessionClose(); if (this.isFullscreen) { this.enableScrollOnRoot(); } diff --git a/public/assets/chat/chat.min.js b/public/assets/chat/chat.min.js index a38a39174..d0a7c4477 100644 --- a/public/assets/chat/chat.min.js +++ b/public/assets/chat/chat.min.js @@ -1 +1 @@ -window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?s(e):""},o=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(t.push('\n\n')),t.push('\n\n '),t.push(n(this.agent.name)),t.push("\n")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,function(){"use strict";var o=Object.hasOwnProperty,i=Object.setPrototypeOf,a=Object.isFrozen,s=Object.getPrototypeOf,r=Object.getOwnPropertyDescriptor,Le=Object.freeze,e=Object.seal,l=Object.create,t="undefined"!=typeof Reflect&&Reflect,d=t.apply,c=t.construct;d||(d=function(e,t,n){return e.apply(t,n)}),Le||(Le=function(e){return e}),e||(e=function(e){return e}),c||(c=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t/gm),rt=e(/^data-[\-\w.\u00B7-\uFFFF]/),lt=e(/^aria-[\-\w]+$/),dt=e(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ct=e(/^(?:\w+script|data):/i),ut=e(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ht="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function mt(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t/i,n))ze(l,e);else{V&&(n=He(n,D," "),n=He(n,R," "));var c=e.nodeName.toLowerCase();if(_e(c,o,n))try{d?e.setAttributeNS(d,l,n):e.setAttribute(l,n),je(u.removed)}catch(e){}}}Ie("afterSanitizeAttributes",e,null)}},Re=function e(t){var n=void 0,o=xe(t);for(Ie("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)Ie("uponSanitizeShadowNode",n,null),Ee(n)||(n.content instanceof h&&e(n.content),De(n));Ie("afterSanitizeShadowDOM",t,null)};return u.sanitize=function(e,t){var n=void 0,o=void 0,s=void 0,i=void 0,a=void 0;if((fe=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Oe(e)){if("function"!=typeof e.toString)throw qe("toString is not a function");if("string"!=typeof(e=e.toString()))throw qe("dirty is not a string, aborting")}if(!u.isSupported){if("object"===ht(d.toStaticHTML)||"function"==typeof d.toStaticHTML){if("string"==typeof e)return d.toStaticHTML(e);if(Oe(e))return d.toStaticHTML(e.outerHTML)}return e}if(Z||be(t),u.removed=[],"string"==typeof e&&(se=!1),se);else if(e instanceof m)1===(o=(n=Ae("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===o.nodeName||"HTML"===o.nodeName?n=o:n.appendChild(o);else{if(!$&&!V&&!G&&-1===e.indexOf("<"))return C&&te?C.createHTML(e):e;if(!(n=Ae(e)))return $?null:S}n&&X&&ke(n.firstChild);for(var r=xe(se?e:n);s=r.nextNode();)3===s.nodeType&&s===i||Ee(s)||(s.content instanceof h&&Re(s.content),De(s),i=s);if(i=null,se)return e;if($){if(J)for(a=x.call(n.ownerDocument);n.firstChild;)a.appendChild(n.firstChild);else a=n;return ee&&(a=I.call(c,a,!0)),a}var l=G?n.outerHTML:n.innerHTML;return V&&(l=He(l,D," "),l=He(l,R," ")),C&&te?C.createHTML(l):l},u.setConfig=function(e){be(e),Z=!0},u.clearConfig=function(){ye=null,Z=!1},u.isValidAttribute=function(e,t,n){ye||be({});var o=Pe(e),s=Pe(t);return _e(o,s,n)},u.addHook=function(e,t){"function"==typeof t&&(_[e]=_[e]||[],Ne(_[e],t))},u.removeHook=function(e){_[e]&&je(_[e])},u.removeHooks=function(e){_[e]&&(_[e]=[])},u.removeAllHooks=function(){_={}},u}()});var bind=function(e,t){return function(){return e.apply(t,arguments)}},slice=[].slice,extend=function(e,t){for(var n in t)hasProp.call(t,n)&&(e[n]=t[n]);function o(){this.constructor=e}return o.prototype=t.prototype,e.prototype=new o,e.__super__=t.prototype,e},hasProp={}.hasOwnProperty;!function(E,_){var n,o,t,s,e,i,a,r,l;i=(l=document.getElementsByTagName("script"))[l.length-1],r=_.location.protocol.replace(":",""),i&&i.src&&(a=i.src.match(".*://([^:/]*).*")[1],r=i.src.match("(.*)://[^:/]*.*")[1]),n=function(){function e(e){this.options=E.extend({},this.defaults,e),this.log=new t({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),t=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=E.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var e;if(e=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",e)},e.prototype.notice=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",e)},e.prototype.error=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("error",e)},e.prototype.log=function(e,t){var n,o,s,i;if(t.unshift("||"),t.unshift(e),t.unshift(this.options.logPrefix),console.log.apply(console,t),this.options.debug){for(i="",o=0,s=t.length;o"+i+"
          ")}},e}(),s=function(e){function t(e){this.stop=bind(this.stop,this),this.start=bind(this.start,this),t.__super__.constructor.call(this,e)}return extend(t,n),t.prototype.timeoutStartedAt=null,t.prototype.logPrefix="timeout",t.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},t.prototype.start=function(){var e,t,n;return this.stop(),t=new Date,e=function(){var e;if(e=new Date-new Date(t.getTime()+1e3*n.options.timeout*60),n.log.debug("Timeout check for "+n.options.timeout+" minutes (left "+e/1e3+" sec.)"),!(e<0))return n.stop(),n.options.callback()},(n=this).log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(e,1e3*this.options.timeoutIntervallCheck*60)},t.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},t}(),o=function(e){function t(e){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),t.__super__.constructor.call(this,e)}return extend(t,n),t.prototype.logPrefix="io",t.prototype.set=function(e){var t,n,o;for(t in n=[],e)o=e[t],n.push(this.options[t]=o);return n},t.prototype.connect=function(){var t,s,n,o;return this.log.debug("Connecting to "+this.options.host),this.ws=new _.WebSocket(""+this.options.host),this.ws.onopen=(t=this,function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}),this.ws.onmessage=(s=this,function(e){var t,n,o;for(o=JSON.parse(e.data),s.log.debug("onMessage",e.data),t=0,n=o.length;tChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5,onReady:void 0,onCloseAnimationEnd:void 0,onError:void 0,onOpenAnimationEnd:void 0,onConnectionReestablished:void 0,onSessionClosed:void 0,onConnectionEstablished:void 0,onCssLoaded:void 0},t.prototype.logPrefix="chat",t.prototype._messageCount=0,t.prototype.isOpen=!1,t.prototype.blinkOnlineInterval=null,t.prototype.stopBlinOnlineStateTimeout=null,t.prototype.showTimeEveryXMinutes=2,t.prototype.lastTimestamp=null,t.prototype.lastAddedType=null,t.prototype.inputDisabled=!1,t.prototype.inputTimeout=null,t.prototype.isTyping=!1,t.prototype.state="offline",t.prototype.initialQueueDelay=1e4,t.prototype.translations={da:{"Chat with us!":"Chat med os!","Scroll down to see new messages":"Scroll ned for at se nye beskeder",Online:"Online",Offline:"Offline",Connecting:"Forbinder","Connection re-established":"Forbindelse genoprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat lukket af %s","Compose your message...":"Skriv en besked...","All colleagues are busy.":"Alle kollegaer er optaget.","You are on waiting list position %s.":"Du er i venteliste som nummer %s.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da du ikke har svaret i de sidste %s minutter er din samtale med %s blevet lukket.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da du ikke har svaret i de sidste %s minutter er din samtale blevet lukket.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, det tager længere end forventet at få en ledig plads. Prøv venligst igen senere eller send os en e-mail. På forhånd tak!"},de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Chat closed by %s":"Chat beendet von %s","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Chat closed by %s":"Chat cerrado por %s","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fi:{"Chat with us!":"Keskustele kanssamme!","Scroll down to see new messages":"Rullaa alas nähdäksesi uudet viestit",Online:"Paikalla",Offline:"Poissa",Connecting:"Yhdistetään","Connection re-established":"Yhteys muodostettu uudelleen",Today:"Tänään",Send:"Lähetä","Chat closed by %s":"%s sulki keskustelun","Compose your message...":"Luo viestisi...","All colleagues are busy.":"Kaikki kollegat ovat varattuja.","You are on waiting list position %s.":"Olet odotuslistalla sijalla %s.","Start new conversation":"Aloita uusi keskustelu","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Koska et vastannut viimeiseen %s minuuttiin, keskustelusi %s kanssa suljettiin.","Since you didn't respond in the last %s minutes your conversation got closed.":"Koska et vastannut viimeiseen %s minuuttiin, keskustelusi suljettiin.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Olemme pahoillamme, tyhjän paikan vapautumisessa kestää odotettua pidempään. Ole hyvä ja yritä myöhemmin uudestaan tai lähetä meille sähköpostia. Kiitos!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Chat closed by %s":"Chat fermé par %s","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collaborateurs sont occupés actuellement.","You are on waiting list position %s.":"Vous êtes actuellement en position %s dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s sera fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Nous vous remercions!"},he:{"Chat with us!":"שוחחאיתנו!","Scroll down to see new messages":"גלול מטה כדי לראות הודעות חדשות",Online:"מחובר",Offline:"מנותק",Connecting:"מתחבר","Connection re-established":"החיבור שוחזר",Today:"היום",Send:"שלח","Chat closed by %s":'הצאט נסגר ע"י %s',"Compose your message...":"כתוב את ההודעה שלך ...","All colleagues are busy.":"כל הנציגים תפוסים","You are on waiting list position %s.":"מיקומך בתור %s.","Start new conversation":"התחל שיחה חדשה","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"מכיוון שלא הגבת במהלך %s דקות השיחה שלך עם %s נסגרה.","Since you didn't respond in the last %s minutes your conversation got closed.":"מכיוון שלא הגבת במהלך %s הדקות האחרונות השיחה שלך נסגרה.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":'מצטערים, הזמן לקבלת נציג ארוך מהרגיל. נסה שוב מאוחר יותר או שלח לנו דוא"ל. תודה!'},hu:{"Chat with us!":"Chatelj velünk!","Scroll down to see new messages":"Görgess lejjebb az újabb üzenetekért",Online:"Online",Offline:"Offline",Connecting:"Csatlakozás","Connection re-established":"Újracsatlakozás",Today:"Ma",Send:"Küldés","Chat closed by %s":"A beszélgetést lezárta %s","Compose your message...":"Írj üzenetet...","All colleagues are busy.":"Jelenleg minden kollégánk elfoglalt.","You are on waiting list position %s.":"A várólistán a %s. pozícióban várakozol.","Start new conversation":"Új beszélgetés indítása","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Mivel %s perce nem érkezett újabb üzenet, ezért a %s kollégával folytatott beszéletést lezártuk.","Since you didn't respond in the last %s minutes your conversation got closed.":"Mivel %s perce nem érkezett válasz, a beszélgetés lezárult.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Sajnáljuk, de a várakozási idő hosszabb a szokásosnál. Kérlek próbáld újra, vagy írd meg kérdésed emailben. Köszönjük!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Chat closed by %s":"Chat gesloten door %s","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},it:{"Chat with us!":"Chatta con noi!","Scroll down to see new messages":"Scorri verso il basso per vedere i nuovi messaggi",Online:"Online",Offline:"Offline",Connecting:"Collegamento in corso","Connection re-established":"Collegamento ristabilito",Today:"Oggi",Send:"Invio","Chat closed by %s":"Chat chiusa da %s","Compose your message...":"Componi il tuo messaggio...","All colleagues are busy.":"Tutti gli operatori sono occupati.","You are on waiting list position %s.":"Sei in posizione %s nella lista d'attesa.","Start new conversation":"Avvia una nuova chat","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat con %s è stata chiusa.","Since you didn't respond in the last %s minutes your conversation got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat è stata chiusa.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Ci dispiace, ci vuole più tempo del previsto per arrivare al tuo turno. Per favore riprova più tardi o inviaci un'email. Grazie!"},pl:{"Chat with us!":"Czatuj z nami!","Scroll down to see new messages":"Przewiń w dół, aby wyświetlić nowe wiadomości",Online:"Online",Offline:"Offline",Connecting:"Łączenie","Connection re-established":"Ponowne nawiązanie połączenia",Today:"dzisiejszy",Send:"Wyślij","Chat closed by %s":"Czat zamknięty przez %s","Compose your message...":"Utwórz swoją wiadomość...","All colleagues are busy.":"Wszyscy koledzy są zajęci.","You are on waiting list position %s.":"Na liście oczekujących znajduje się pozycja %s.","Start new conversation":"Rozpoczęcie nowej konwersacji","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!"},"pt-br":{"Chat with us!":"Chat fale conosco!","Scroll down to see new messages":"Role para baixo, para ver nosvas mensagens",Online:"Online",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexão restabelecida",Today:"Hoje",Send:"Enviar","Chat closed by %s":"Chat encerrado por %s","Compose your message...":"Escreva sua mensagem...","All colleagues are busy.":"Todos os agentes estão ocupados.","You are on waiting list position %s.":"Você está na posição %s na fila de espera.","Start new conversation":"Iniciar uma nova conversa","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Como você não respondeu nos últimos %s minutos sua conversa com %s foi encerrada.","Since you didn't respond in the last %s minutes your conversation got closed.":"Como você não respondeu nos últimos %s minutos sua conversa foi encerrada.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Desculpe, mas o tempo de espera por um agente foi excedido. Tente novamente mais tarde ou nós envie um email. Obrigado"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"},ru:{"Chat with us!":"Напишите нам!","Scroll down to see new messages":"Прокрутите, чтобы увидеть новые сообщения",Online:"Онлайн",Offline:"Оффлайн",Connecting:"Подключение","Connection re-established":"Подключение восстановлено",Today:"Сегодня",Send:"Отправить","Chat closed by %s":"%s закрыл чат","Compose your message...":"Напишите сообщение...","All colleagues are busy.":"Все сотрудники заняты","You are on waiting list position %s.":"Вы в списке ожидания под номером %s","Start new conversation":"Начать новую переписку.","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.","Since you didn't respond in the last %s minutes your conversation got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!"},sv:{"Chat with us!":"Chatta med oss!","Scroll down to see new messages":"Rulla ner för att se nya meddelanden",Online:"Online",Offline:"Offline",Connecting:"Ansluter","Connection re-established":"Anslutningen återupprättas",Today:"I dag",Send:"Skicka","Chat closed by %s":"Chatt stängd av %s","Compose your message...":"Skriv ditt meddelande...","All colleagues are busy.":"Alla kollegor är upptagna.","You are on waiting list position %s.":"Du är på väntelistan som position %s.","Start new conversation":"Starta ny konversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Eftersom du inte svarat inom %s minuterna i din konversation med %s så stängdes chatten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Då du inte svarat inom de senaste %s minuterna så avslutades din chatt.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi är ledsna, det tar längre tid som förväntat att få en ledig plats. Försök igen senare eller skicka ett e-postmeddelande till oss. Tack!"},no:{"Chat with us!":"Chat med oss!","Scroll down to see new messages":"Bla ned for å se nye meldinger",Online:"Pålogget",Offline:"Avlogget",Connecting:"Koble til","Connection re-established":"Tilkoblingen er gjenopprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat avsluttes om %s","Compose your message...":"Skriv din melding...","All colleagues are busy.":"Alle våre kolleger er for øyeblikket opptatt.","You are on waiting list position %s.":"Du står nå i kø og er nr. %s på ventelisten.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med %s nå avsluttes.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!"},nb:{"Chat with us!":"Chat med oss!","Scroll down to see new messages":"Bla ned for å se nye meldinger",Online:"Pålogget",Offline:"Avlogget",Connecting:"Koble til","Connection re-established":"Tilkoblingen er gjenopprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat avsluttes om %s","Compose your message...":"Skriv din melding...","All colleagues are busy.":"Alle våre kolleger er for øyeblikket opptatt.","You are on waiting list position %s.":"Du står nå i kø og er nr. %s på ventelisten.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med %s nå avsluttes.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!"},el:{"Chat with us!":"Επικοινωνήστε μαζί μας!","Scroll down to see new messages":"Μεταβείτε κάτω για να δείτε τα νέα μηνύματα",Online:"Σε σύνδεση",Offline:"Αποσυνδεμένος",Connecting:"Σύνδεση","Connection re-established":"Η σύνδεση αποκαταστάθηκε",Today:"Σήμερα",Send:"Αποστολή","Chat closed by %s":"Η συνομιλία έκλεισε από τον/την %s","Compose your message...":"Γράψτε το μήνυμα σας...","All colleagues are busy.":"Όλοι οι συνάδελφοι μας είναι απασχολημένοι.","You are on waiting list position %s.":"Βρίσκεστε σε λίστα αναμονής στη θέση %s.","Start new conversation":"Έναρξη νέας συνομιλίας","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας με τον/την %s έκλεισε.","Since you didn't respond in the last %s minutes your conversation got closed.":"Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας έκλεισε.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Λυπούμαστε που χρειάζεται περισσότερος χρόνος από τον αναμενόμενο για να βρεθεί μία κενή θέση. Παρακαλούμε δοκιμάστε ξανά αργότερα ή στείλτε μας ένα email. Ευχαριστούμε!"}},t.prototype.sessionId=void 0,t.prototype.scrolledToBottom=!0,t.prototype.scrollSnapTolerance=10,t.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},t.prototype.T=function(){var e,t,n,o,s,i;if(s=arguments[0],t=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?((i=this.translations[this.options.lang])[s]||this.log.notice("Translation needed for '"+s+"'"),s=i[s]||s):this.log.notice("Translation '"+this.options.lang+"' needed!")),t)for(n=0,o=t.length;n',document.execCommand("insertHTML",!1,s)},m.resizeImage(s.src,460,"auto",2,"image/jpeg","auto",t)},d.readAsDataURL(i),a=!0)),!a){o=h=void 0;try{h=n.getData("text/html"),o="html",h&&0!==h.length||(o="text",h=n.getData("text/plain")),h&&0!==h.length||(o="text2",h=n.getData("text"))}catch(e){t=e,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",h=n.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(h=(h="
          "+h.replace(/\n/g,"
          ")+"
          ").replace(/
          <\/div>/g,"

          ")),console.log("p",o,h),"html"===o&&(u=DOMPurify.sanitize(h),m.log.debug("sanitized HTML clipboard",u),e=E("
          "+u+"
          "),l=!1,s=h,c=new RegExp("<(/w|w):[A-Za-z]"),s.match(c)&&(l=!0,s=s.replace(c,"")),c=new RegExp("<(/o|o):[A-Za-z]"),s.match(c)&&(l=!0,s=s.replace(c,"")),l&&(e=m.wordFilter(e)),(e=E(e)).contents().each(function(){if(8===this.nodeType)return E(this).remove()}),e.find("a, font, small, time, form, label").replaceWith(function(){return E(this).contents()}),e.find("textarea").each(function(){var e,t;return t=this.outerHTML,c=new RegExp("<"+this.tagName,"i"),e=t.replace(c,"')).get(0),document.caretPositionFromPoint?(s=document.caretPositionFromPoint(l,d),(i=document.createRange()).setStart(s.offsetNode,s.offset),i.collapse(),i.insertNode(a)):document.caretRangeFromPoint?(i=document.caretRangeFromPoint(l,d)).insertNode(a):console.log("could not find carat")},c.resizeImage(a.src,460,"auto",2,"image/jpeg","auto",t)},o.readAsDataURL(n)})),E(_).on("beforeunload",(e=this,function(){return e.onLeaveTemporary()})),E(_).bind("hashchange",(t=this,function(){if(!t.isOpen)return t.idleTimeout.start();t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:_.location.href})})),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},t.prototype.stopPropagation=function(e){return e.stopPropagation()},t.prototype.checkForEnter=function(e){if(!this.inputDisabled&&!e.shiftKey&&13===e.keyCode)return e.preventDefault(),this.sendMessage()},t.prototype.send=function(e,t){return null==t&&(t={}),t.chat_id=this.options.chatId,this.io.send(e,t)},t.prototype.onWebSocketMessage=function(e){var t,n,o;for(t=0,n=e.length;tnew Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},t.prototype.onSubmit=function(e){return e.preventDefault(),this.sendMessage()},t.prototype.sendMessage=function(){var e,t;if(e=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),t=this.view("message")({message:e,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(t)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(t)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:e,id:this._messageCount,session_id:this.sessionId})},t.prototype.receiveMessage=function(e){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:e.message.content,id:e.id,from:"agent"}),this.scrollToBottom({showHint:!0})},t.prototype.renderMessage=function(e){return this.lastAddedType="message--"+e.from,e.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(e))},t.prototype.open=function(){var e;if(!this.isOpen)return this.isOpen=!0,this.log.debug("open widget"),this.show(),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-e),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:_.location.href}));this.log.debug("widget already open, block")},t.prototype.onOpenAnimationEnd=function(){var e;return this.idleTimeout.stop(),this.isFullscreen&&this.disableScrollOnRoot(),"function"==typeof(e=this.options).onOpenAnimationEnd?e.onOpenAnimationEnd():void 0},t.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},t.prototype.toggle=function(e){return this.isOpen?this.close(e):this.open(e)},t.prototype.close=function(e){var t;if(this.isOpen){if(this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId)return this.log.debug("close widget"),e&&e.stopPropagation(),this.sessionClose(),this.isFullscreen&&this.enableScrollOnRoot(),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-t},500,this.onCloseAnimationEnd);this.log.debug("can't close widget without sessionId")}else this.log.debug("can't close widget, it's not open")},t.prototype.onCloseAnimationEnd=function(){var e;return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,"function"==typeof(e=this.options).onCloseAnimationEnd&&e.onCloseAnimationEnd(),this.io.reconnect()},t.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},t.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},t.prototype.disableInput=function(){return this.inputDisabled=!0,this.input.prop("contenteditable",!1),this.el.find(".zammad-chat-send").prop("disabled",!0),this.io.close()},t.prototype.enableInput=function(){return this.inputDisabled=!1,this.input.prop("contenteditable",!0),this.el.find(".zammad-chat-send").prop("disabled",!1)},t.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},t.prototype.onQueueScreen=function(e){var t,n;if(this.setSessionId(e.session_id),t=function(){return n.onQueue(e),n.waitingListTimeout.start()},!(n=this).initialQueueDelay||this.onInitialQueueDelayId)return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),t();this.onInitialQueueDelayId=setTimeout(t,this.initialQueueDelay)},t.prototype.onQueue=function(e){return this.log.notice("onQueue",e.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:e.position}))},t.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},t.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},t.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},t.prototype.maybeAddTimestamp=function(){var e,t,n;if(n=Date.now(),!this.lastTimestamp||n-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return e=this.T("Today"),t=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(e,t),this.lastTimestamp=n):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:e,time:t})),this.lastTimestamp=n,this.lastAddedType="timestamp",this.scrollToBottom())},t.prototype.updateLastTimestamp=function(e,t){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:e,time:t}))},t.prototype.addStatus=function(e){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:e})),this.scrollToBottom()},t.prototype.detectScrolledtoBottom=function(){var e;if(e=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(e-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},t.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},t.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},t.prototype.scrollToBottom=function(e){var t;return t=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(E(".zammad-chat-body").prop("scrollHeight")):t?this.showScrollHint():void 0},t.prototype.destroy=function(e){return null==e&&(e={}),this.log.debug("destroy widget",e),this.setAgentOnlineState("offline"),e.remove&&this.el&&(this.el.remove(),E("."+this.options.buttonClass).hide()),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},t.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},t.prototype.onConnectionReestablished=function(){var e;return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established")),"function"==typeof(e=this.options).onConnectionReestablished?e.onConnectionReestablished():void 0},t.prototype.onSessionClosed=function(e){var t;return this.addStatus(this.T("Chat closed by %s",e.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop(),"function"==typeof(t=this.options).onSessionClosed?t.onSessionClosed(e):void 0},t.prototype.setSessionId=function(e){return void 0===(this.sessionId=e)?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",e)},t.prototype.onConnectionEstablished=function(e){var t;return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,e.agent&&(this.agent=e.agent),e.session_id&&this.setSessionId(e.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start(),"function"==typeof(t=this.options).onConnectionEstablished?t.onConnectionEstablished(e):void 0},t.prototype.showCustomerTimeout=function(){var e;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),e=function(){return location.reload()},this.el.find(".js-restart").click(e),this.sessionClose()},t.prototype.showWaitingListTimeout=function(){var e;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),e=function(){return location.reload()},this.el.find(".js-restart").click(e),this.sessionClose()},t.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},t.prototype.setAgentOnlineState=function(e){var t;if(this.state=e,this.el)return t=e.charAt(0).toUpperCase()+e.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",e).text(this.T(t))},t.prototype.detectHost=function(){var e;return e="ws://","https"===r&&(e="wss://"),this.options.host=""+e+a+"/ws"},t.prototype.loadCss=function(){var e,t,n;if(this.options.cssAutoload)return(n=this.options.cssUrl)||(n=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws$/i,""),n+="/assets/chat/chat.css"),this.log.debug("load css from '"+n+"'"),t="@import url('"+n+"');",(e=document.createElement("link")).onload=this.onCssLoaded,e.rel="stylesheet",e.href="data:text/css,"+escape(t),document.getElementsByTagName("head")[0].appendChild(e)},t.prototype.onCssLoaded=function(){var e;return this.cssLoaded=!0,this.socketReady&&this.onReady(),"function"==typeof(e=this.options).onCssLoaded?e.onCssLoaded():void 0},t.prototype.startTimeoutObservers=function(){var e,t,n;return this.idleTimeout=new s({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:(e=this,function(){return e.log.debug("Idle timeout reached, hide widget",new Date),e.destroy({remove:!0})})}),this.inactiveTimeout=new s({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:(t=this,function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})})}),this.waitingListTimeout=new s({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:(n=this,function(){return n.log.debug("Waiting list timeout reached, show timeout screen.",new Date),n.showWaitingListTimeout(),n.destroy({remove:!1})})})},t.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},t.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},t.prototype.isVisible=function(e,t,n,o){var s,i,a,r,l,d,c,u,h,m,p,g,f,y,v,b,w,T,C,S,k,z,A,x,O,I;if(!(e.length<1))if(i=E(_),T=(s=1/gi,"")).replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,"")).replace(/<(\/?)s>/gi,"<$1strike>")).replace(/ /gi," "),e.html(t),E("p",e).each(function(){var e,t;if(t=E(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(t))return E(this).data("_listLevel",parseInt(e[1],10))}),c=0,u=null,E("p",e).each(function(){var e,t,n,o,s,i,a,r,l,d;if(void 0!==(e=E(this).data("_listLevel"))){if(d=E(this).text(),o="
            ",/^\s*\w+\./.test(d)&&(o=(s=/([0-9])\./.exec(d))?null!=(i=1<(l=parseInt(s[1],10)))?i:'
              ':"
                "}:"
                  "),c"+E(this).html()+""),E(this).remove(),c=e}return c=0}),E("[style]",e).removeAttr("style"),E("[align]",e).removeAttr("align"),E("span",e).replaceWith(function(){return E(this).contents()}),E("span:empty",e).remove(),E("[class^='Mso']",e).removeAttr("class"),E("p:empty",e).remove(),e},t.prototype.removeAttribute=function(e){var t,n,o,s,i;if(e){for(t=E(e),o=0,s=(i=e.attributes).length;o/g,">").replace(/"/g,""")}),function(){(function(){t.push('
                  \n
                  \n
                  \n \n \n \n \n \n
                  \n
                  \n
                  \n
                  \n \n '),t.push(this.T(this.title)),t.push('\n
                  \n
                  \n
                  \n \n
                  \n
                  \n
                  \n \n
                  \n
                  ")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
                  \n '),this.agent?(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent))):(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay))),t.push("\n "),t.push('\n
                  \n
                  "),t.push(this.T("Start new conversation")),t.push("
                  \n
                  ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('\n \n \n \n\n'),t.push(this.T("Connecting")),t.push("")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?s(e):""},o=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                  \n "),t.push(this.message),t.push("\n
                  ")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                  \n
                  \n '),t.push(this.status),t.push("\n
                  \n
                  ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?s(e):""},o=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                  '),t.push(n(this.label)),t.push(" "),t.push(n(this.time)),t.push("
                  ")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                  \n \n \n \n \n \n \n \n
                  ')}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                  \n \n \n \n \n \n '),t.push(this.T("All colleagues are busy.")),t.push("
                  \n "),t.push(this.T("You are on waiting list position %s.",this.position)),t.push("\n
                  ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
                  \n '),t.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),t.push('\n
                  \n
                  "),t.push(this.T("Start new conversation")),t.push("
                  \n
                  ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")}; \ No newline at end of file +window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.agent=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?s(e):""},o=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){this.agent.avatar&&(t.push('\n\n')),t.push('\n\n '),t.push(n(this.agent.name)),t.push("\n")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).DOMPurify=t()}(this,function(){"use strict";var o=Object.hasOwnProperty,i=Object.setPrototypeOf,a=Object.isFrozen,s=Object.getPrototypeOf,r=Object.getOwnPropertyDescriptor,Le=Object.freeze,e=Object.seal,l=Object.create,t="undefined"!=typeof Reflect&&Reflect,d=t.apply,c=t.construct;d||(d=function(e,t,n){return e.apply(t,n)}),Le||(Le=function(e){return e}),e||(e=function(e){return e}),c||(c=function(e,t){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t/gm),rt=e(/^data-[\-\w.\u00B7-\uFFFF]/),lt=e(/^aria-[\-\w]+$/),dt=e(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ct=e(/^(?:\w+script|data):/i),ut=e(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ht="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function mt(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t/i,n))ze(l,e);else{V&&(n=He(n,D," "),n=He(n,R," "));var c=e.nodeName.toLowerCase();if(_e(c,o,n))try{d?e.setAttributeNS(d,l,n):e.setAttribute(l,n),je(u.removed)}catch(e){}}}Ie("afterSanitizeAttributes",e,null)}},Re=function e(t){var n=void 0,o=xe(t);for(Ie("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)Ie("uponSanitizeShadowNode",n,null),Ee(n)||(n.content instanceof h&&e(n.content),De(n));Ie("afterSanitizeShadowDOM",t,null)};return u.sanitize=function(e,t){var n=void 0,o=void 0,s=void 0,i=void 0,a=void 0;if((fe=!e)&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Oe(e)){if("function"!=typeof e.toString)throw qe("toString is not a function");if("string"!=typeof(e=e.toString()))throw qe("dirty is not a string, aborting")}if(!u.isSupported){if("object"===ht(d.toStaticHTML)||"function"==typeof d.toStaticHTML){if("string"==typeof e)return d.toStaticHTML(e);if(Oe(e))return d.toStaticHTML(e.outerHTML)}return e}if(Z||be(t),u.removed=[],"string"==typeof e&&(se=!1),se);else if(e instanceof m)1===(o=(n=Ae("\x3c!----\x3e")).ownerDocument.importNode(e,!0)).nodeType&&"BODY"===o.nodeName||"HTML"===o.nodeName?n=o:n.appendChild(o);else{if(!$&&!V&&!G&&-1===e.indexOf("<"))return C&&te?C.createHTML(e):e;if(!(n=Ae(e)))return $?null:S}n&&X&&ke(n.firstChild);for(var r=xe(se?e:n);s=r.nextNode();)3===s.nodeType&&s===i||Ee(s)||(s.content instanceof h&&Re(s.content),De(s),i=s);if(i=null,se)return e;if($){if(J)for(a=x.call(n.ownerDocument);n.firstChild;)a.appendChild(n.firstChild);else a=n;return ee&&(a=I.call(c,a,!0)),a}var l=G?n.outerHTML:n.innerHTML;return V&&(l=He(l,D," "),l=He(l,R," ")),C&&te?C.createHTML(l):l},u.setConfig=function(e){be(e),Z=!0},u.clearConfig=function(){ye=null,Z=!1},u.isValidAttribute=function(e,t,n){ye||be({});var o=Pe(e),s=Pe(t);return _e(o,s,n)},u.addHook=function(e,t){"function"==typeof t&&(_[e]=_[e]||[],Ne(_[e],t))},u.removeHook=function(e){_[e]&&je(_[e])},u.removeHooks=function(e){_[e]&&(_[e]=[])},u.removeAllHooks=function(){_={}},u}()});var bind=function(e,t){return function(){return e.apply(t,arguments)}},slice=[].slice,extend=function(e,t){for(var n in t)hasProp.call(t,n)&&(e[n]=t[n]);function o(){this.constructor=e}return o.prototype=t.prototype,e.prototype=new o,e.__super__=t.prototype,e},hasProp={}.hasOwnProperty;!function(E,_){var n,o,t,s,e,i,a,r,l;i=(l=document.getElementsByTagName("script"))[l.length-1],r=_.location.protocol.replace(":",""),i&&i.src&&(a=i.src.match(".*://([^:/]*).*")[1],r=i.src.match("(.*)://[^:/]*.*")[1]),n=function(){function e(e){this.options=E.extend({},this.defaults,e),this.log=new t({debug:this.options.debug,logPrefix:this.options.logPrefix||this.logPrefix})}return e.prototype.defaults={debug:!1},e}(),t=function(){function e(e){this.log=bind(this.log,this),this.error=bind(this.error,this),this.notice=bind(this.notice,this),this.debug=bind(this.debug,this),this.options=E.extend({},this.defaults,e)}return e.prototype.defaults={debug:!1},e.prototype.debug=function(){var e;if(e=1<=arguments.length?slice.call(arguments,0):[],this.options.debug)return this.log("debug",e)},e.prototype.notice=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("notice",e)},e.prototype.error=function(){var e;return e=1<=arguments.length?slice.call(arguments,0):[],this.log("error",e)},e.prototype.log=function(e,t){var n,o,s,i;if(t.unshift("||"),t.unshift(e),t.unshift(this.options.logPrefix),console.log.apply(console,t),this.options.debug){for(i="",o=0,s=t.length;o"+i+"
                  ")}},e}(),s=function(e){function t(e){this.stop=bind(this.stop,this),this.start=bind(this.start,this),t.__super__.constructor.call(this,e)}return extend(t,n),t.prototype.timeoutStartedAt=null,t.prototype.logPrefix="timeout",t.prototype.defaults={debug:!1,timeout:4,timeoutIntervallCheck:.5},t.prototype.start=function(){var e,t,n;return this.stop(),t=new Date,e=function(){var e;if(e=new Date-new Date(t.getTime()+1e3*n.options.timeout*60),n.log.debug("Timeout check for "+n.options.timeout+" minutes (left "+e/1e3+" sec.)"),!(e<0))return n.stop(),n.options.callback()},(n=this).log.debug("Start timeout in "+this.options.timeout+" minutes"),this.intervallId=setInterval(e,1e3*this.options.timeoutIntervallCheck*60)},t.prototype.stop=function(){if(this.intervallId)return this.log.debug("Stop timeout of "+this.options.timeout+" minutes"),clearInterval(this.intervallId)},t}(),o=function(e){function t(e){this.ping=bind(this.ping,this),this.send=bind(this.send,this),this.reconnect=bind(this.reconnect,this),this.close=bind(this.close,this),this.connect=bind(this.connect,this),this.set=bind(this.set,this),t.__super__.constructor.call(this,e)}return extend(t,n),t.prototype.logPrefix="io",t.prototype.set=function(e){var t,n,o;for(t in n=[],e)o=e[t],n.push(this.options[t]=o);return n},t.prototype.connect=function(){var t,s,n,o;return this.log.debug("Connecting to "+this.options.host),this.ws=new _.WebSocket(""+this.options.host),this.ws.onopen=(t=this,function(e){return t.log.debug("onOpen",e),t.options.onOpen(e),t.ping()}),this.ws.onmessage=(s=this,function(e){var t,n,o;for(o=JSON.parse(e.data),s.log.debug("onMessage",e.data),t=0,n=o.length;tChat with us!",scrollHint:"Scroll down to see new messages",idleTimeout:6,idleTimeoutIntervallCheck:.5,inactiveTimeout:8,inactiveTimeoutIntervallCheck:.5,waitingListTimeout:4,waitingListTimeoutIntervallCheck:.5,onReady:void 0,onCloseAnimationEnd:void 0,onError:void 0,onOpenAnimationEnd:void 0,onConnectionReestablished:void 0,onSessionClosed:void 0,onConnectionEstablished:void 0,onCssLoaded:void 0},t.prototype.logPrefix="chat",t.prototype._messageCount=0,t.prototype.isOpen=!1,t.prototype.blinkOnlineInterval=null,t.prototype.stopBlinOnlineStateTimeout=null,t.prototype.showTimeEveryXMinutes=2,t.prototype.lastTimestamp=null,t.prototype.lastAddedType=null,t.prototype.inputDisabled=!1,t.prototype.inputTimeout=null,t.prototype.isTyping=!1,t.prototype.state="offline",t.prototype.initialQueueDelay=1e4,t.prototype.translations={da:{"Chat with us!":"Chat med os!","Scroll down to see new messages":"Scroll ned for at se nye beskeder",Online:"Online",Offline:"Offline",Connecting:"Forbinder","Connection re-established":"Forbindelse genoprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat lukket af %s","Compose your message...":"Skriv en besked...","All colleagues are busy.":"Alle kollegaer er optaget.","You are on waiting list position %s.":"Du er i venteliste som nummer %s.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da du ikke har svaret i de sidste %s minutter er din samtale med %s blevet lukket.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da du ikke har svaret i de sidste %s minutter er din samtale blevet lukket.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, det tager længere end forventet at få en ledig plads. Prøv venligst igen senere eller send os en e-mail. På forhånd tak!"},de:{"Chat with us!":"Chatte mit uns!","Scroll down to see new messages":"Scrolle nach unten um neue Nachrichten zu sehen",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbindung wiederhergestellt",Today:"Heute",Send:"Senden","Chat closed by %s":"Chat beendet von %s","Compose your message...":"Ihre Nachricht...","All colleagues are busy.":"Alle Kollegen sind belegt.","You are on waiting list position %s.":"Sie sind in der Warteliste an der Position %s.","Start new conversation":"Neue Konversation starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit %s geschlossen.","Since you didn't respond in the last %s minutes your conversation got closed.":"Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!"},es:{"Chat with us!":"Chatee con nosotros!","Scroll down to see new messages":"Haga scroll hacia abajo para ver nuevos mensajes",Online:"En linea",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexión restablecida",Today:"Hoy",Send:"Enviar","Chat closed by %s":"Chat cerrado por %s","Compose your message...":"Escriba su mensaje...","All colleagues are busy.":"Todos los agentes están ocupados.","You are on waiting list position %s.":"Usted está en la posición %s de la lista de espera.","Start new conversation":"Iniciar nueva conversación","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación con %s se ha cerrado.","Since you didn't respond in the last %s minutes your conversation got closed.":"Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!"},fi:{"Chat with us!":"Keskustele kanssamme!","Scroll down to see new messages":"Rullaa alas nähdäksesi uudet viestit",Online:"Paikalla",Offline:"Poissa",Connecting:"Yhdistetään","Connection re-established":"Yhteys muodostettu uudelleen",Today:"Tänään",Send:"Lähetä","Chat closed by %s":"%s sulki keskustelun","Compose your message...":"Luo viestisi...","All colleagues are busy.":"Kaikki kollegat ovat varattuja.","You are on waiting list position %s.":"Olet odotuslistalla sijalla %s.","Start new conversation":"Aloita uusi keskustelu","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Koska et vastannut viimeiseen %s minuuttiin, keskustelusi %s kanssa suljettiin.","Since you didn't respond in the last %s minutes your conversation got closed.":"Koska et vastannut viimeiseen %s minuuttiin, keskustelusi suljettiin.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Olemme pahoillamme, tyhjän paikan vapautumisessa kestää odotettua pidempään. Ole hyvä ja yritä myöhemmin uudestaan tai lähetä meille sähköpostia. Kiitos!"},fr:{"Chat with us!":"Chattez avec nous!","Scroll down to see new messages":"Faites défiler pour lire les nouveaux messages",Online:"En-ligne",Offline:"Hors-ligne",Connecting:"Connexion en cours","Connection re-established":"Connexion rétablie",Today:"Aujourdhui",Send:"Envoyer","Chat closed by %s":"Chat fermé par %s","Compose your message...":"Composez votre message...","All colleagues are busy.":"Tous les collaborateurs sont occupés actuellement.","You are on waiting list position %s.":"Vous êtes actuellement en position %s dans la file d'attente.","Start new conversation":"Démarrer une nouvelle conversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation avec %s sera fermée.","Since you didn't respond in the last %s minutes your conversation got closed.":"Si vous ne répondez pas dans les %s minutes, votre conversation va être fermée.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Nous sommes désolés, il faut plus de temps que prévu pour obtenir un emplacement vide. Veuillez réessayer ultérieurement ou nous envoyer un courriel. Nous vous remercions!"},he:{"Chat with us!":"שוחחאיתנו!","Scroll down to see new messages":"גלול מטה כדי לראות הודעות חדשות",Online:"מחובר",Offline:"מנותק",Connecting:"מתחבר","Connection re-established":"החיבור שוחזר",Today:"היום",Send:"שלח","Chat closed by %s":'הצאט נסגר ע"י %s',"Compose your message...":"כתוב את ההודעה שלך ...","All colleagues are busy.":"כל הנציגים תפוסים","You are on waiting list position %s.":"מיקומך בתור %s.","Start new conversation":"התחל שיחה חדשה","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"מכיוון שלא הגבת במהלך %s דקות השיחה שלך עם %s נסגרה.","Since you didn't respond in the last %s minutes your conversation got closed.":"מכיוון שלא הגבת במהלך %s הדקות האחרונות השיחה שלך נסגרה.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":'מצטערים, הזמן לקבלת נציג ארוך מהרגיל. נסה שוב מאוחר יותר או שלח לנו דוא"ל. תודה!'},hu:{"Chat with us!":"Chatelj velünk!","Scroll down to see new messages":"Görgess lejjebb az újabb üzenetekért",Online:"Online",Offline:"Offline",Connecting:"Csatlakozás","Connection re-established":"Újracsatlakozás",Today:"Ma",Send:"Küldés","Chat closed by %s":"A beszélgetést lezárta %s","Compose your message...":"Írj üzenetet...","All colleagues are busy.":"Jelenleg minden kollégánk elfoglalt.","You are on waiting list position %s.":"A várólistán a %s. pozícióban várakozol.","Start new conversation":"Új beszélgetés indítása","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Mivel %s perce nem érkezett újabb üzenet, ezért a %s kollégával folytatott beszéletést lezártuk.","Since you didn't respond in the last %s minutes your conversation got closed.":"Mivel %s perce nem érkezett válasz, a beszélgetés lezárult.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Sajnáljuk, de a várakozási idő hosszabb a szokásosnál. Kérlek próbáld újra, vagy írd meg kérdésed emailben. Köszönjük!"},nl:{"Chat with us!":"Chat met ons!","Scroll down to see new messages":"Scrol naar beneden om nieuwe berichten te zien",Online:"Online",Offline:"Offline",Connecting:"Verbinden","Connection re-established":"Verbinding herstelt",Today:"Vandaag",Send:"Verzenden","Chat closed by %s":"Chat gesloten door %s","Compose your message...":"Typ uw bericht...","All colleagues are busy.":"Alle medewerkers zijn bezet.","You are on waiting list position %s.":"U bent %s in de wachtrij.","Start new conversation":"Nieuwe conversatie starten","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft wordt de conversatie met %s gesloten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Omdat u in de laatste %s minuten niets geschreven heeft is de conversatie gesloten.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Het spijt ons, het duurt langer dan verwacht om te antwoorden. Alstublieft probeer het later nogmaals of stuur ons een email. Hartelijk dank!"},it:{"Chat with us!":"Chatta con noi!","Scroll down to see new messages":"Scorri verso il basso per vedere i nuovi messaggi",Online:"Online",Offline:"Offline",Connecting:"Collegamento in corso","Connection re-established":"Collegamento ristabilito",Today:"Oggi",Send:"Invio","Chat closed by %s":"Chat chiusa da %s","Compose your message...":"Componi il tuo messaggio...","All colleagues are busy.":"Tutti gli operatori sono occupati.","You are on waiting list position %s.":"Sei in posizione %s nella lista d'attesa.","Start new conversation":"Avvia una nuova chat","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat con %s è stata chiusa.","Since you didn't respond in the last %s minutes your conversation got closed.":"Dal momento che non hai risposto negli ultimi %s minuti la tua chat è stata chiusa.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Ci dispiace, ci vuole più tempo del previsto per arrivare al tuo turno. Per favore riprova più tardi o inviaci un'email. Grazie!"},pl:{"Chat with us!":"Czatuj z nami!","Scroll down to see new messages":"Przewiń w dół, aby wyświetlić nowe wiadomości",Online:"Online",Offline:"Offline",Connecting:"Łączenie","Connection re-established":"Ponowne nawiązanie połączenia",Today:"dzisiejszy",Send:"Wyślij","Chat closed by %s":"Czat zamknięty przez %s","Compose your message...":"Utwórz swoją wiadomość...","All colleagues are busy.":"Wszyscy koledzy są zajęci.","You are on waiting list position %s.":"Na liście oczekujących znajduje się pozycja %s.","Start new conversation":"Rozpoczęcie nowej konwersacji","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ponieważ w ciągu ostatnich %s minut nie odpowiedziałeś, Twoja rozmowa z %s została zamknięta.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ponieważ nie odpowiedziałeś w ciągu ostatnich %s minut, Twoja rozmowa została zamknięta.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Przykro nam, ale to trwa dłużej niż się spodziewamy. Spróbuj ponownie później lub wyślij nam wiadomość e-mail. Dziękuję!"},"pt-br":{"Chat with us!":"Chat fale conosco!","Scroll down to see new messages":"Role para baixo, para ver nosvas mensagens",Online:"Online",Offline:"Desconectado",Connecting:"Conectando","Connection re-established":"Conexão restabelecida",Today:"Hoje",Send:"Enviar","Chat closed by %s":"Chat encerrado por %s","Compose your message...":"Escreva sua mensagem...","All colleagues are busy.":"Todos os agentes estão ocupados.","You are on waiting list position %s.":"Você está na posição %s na fila de espera.","Start new conversation":"Iniciar uma nova conversa","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Como você não respondeu nos últimos %s minutos sua conversa com %s foi encerrada.","Since you didn't respond in the last %s minutes your conversation got closed.":"Como você não respondeu nos últimos %s minutos sua conversa foi encerrada.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Desculpe, mas o tempo de espera por um agente foi excedido. Tente novamente mais tarde ou nós envie um email. Obrigado"},"zh-cn":{"Chat with us!":"发起即时对话!","Scroll down to see new messages":"向下滚动以查看新消息",Online:"在线",Offline:"离线",Connecting:"连接中","Connection re-established":"正在重新建立连接",Today:"今天",Send:"发送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在输入信息...","All colleagues are busy.":"所有工作人员都在忙碌中.","You are on waiting list position %s.":"您目前的等候位置是第 %s 位.","Start new conversation":"开始新的会话","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由于您超过 %s 分钟没有回复, 您与 %s 的会话已被关闭.","Since you didn't respond in the last %s minutes your conversation got closed.":"由于您超过 %s 分钟没有任何回复, 该对话已被关闭.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 目前需要等候更长的时间才能接入对话, 请稍后重试或向我们发送电子邮件. 谢谢!"},"zh-tw":{"Chat with us!":"開始即時對话!","Scroll down to see new messages":"向下滑動以查看新訊息",Online:"線上",Offline:"离线",Connecting:"連線中","Connection re-established":"正在重新建立連線中",Today:"今天",Send:"發送","Chat closed by %s":"Chat closed by %s","Compose your message...":"正在輸入訊息...","All colleagues are busy.":"所有服務人員都在忙碌中.","You are on waiting list position %s.":"你目前的等候位置是第 %s 順位.","Start new conversation":"開始新的對話","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"由於你超過 %s 分鐘沒有回應, 你與 %s 的對話已被關閉.","Since you didn't respond in the last %s minutes your conversation got closed.":"由於你超過 %s 分鐘沒有任何回應, 該對話已被關閉.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"非常抱歉, 當前需要等候更長的時間方可排入對話程序, 請稍後重試或向我們寄送電子郵件. 謝謝!"},ru:{"Chat with us!":"Напишите нам!","Scroll down to see new messages":"Прокрутите, чтобы увидеть новые сообщения",Online:"Онлайн",Offline:"Оффлайн",Connecting:"Подключение","Connection re-established":"Подключение восстановлено",Today:"Сегодня",Send:"Отправить","Chat closed by %s":"%s закрыл чат","Compose your message...":"Напишите сообщение...","All colleagues are busy.":"Все сотрудники заняты","You are on waiting list position %s.":"Вы в списке ожидания под номером %s","Start new conversation":"Начать новую переписку.","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор с %s был закрыт.","Since you didn't respond in the last %s minutes your conversation got closed.":"Поскольку вы не отвечали в течение последних %s минут, ваш разговор был закрыт.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"К сожалению, ожидание свободного места требует больше времени. Повторите попытку позже или отправьте нам электронное письмо. Спасибо!"},sv:{"Chat with us!":"Chatta med oss!","Scroll down to see new messages":"Rulla ner för att se nya meddelanden",Online:"Online",Offline:"Offline",Connecting:"Ansluter","Connection re-established":"Anslutningen återupprättas",Today:"I dag",Send:"Skicka","Chat closed by %s":"Chatt stängd av %s","Compose your message...":"Skriv ditt meddelande...","All colleagues are busy.":"Alla kollegor är upptagna.","You are on waiting list position %s.":"Du är på väntelistan som position %s.","Start new conversation":"Starta ny konversation","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Eftersom du inte svarat inom %s minuterna i din konversation med %s så stängdes chatten.","Since you didn't respond in the last %s minutes your conversation got closed.":"Då du inte svarat inom de senaste %s minuterna så avslutades din chatt.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi är ledsna, det tar längre tid som förväntat att få en ledig plats. Försök igen senare eller skicka ett e-postmeddelande till oss. Tack!"},no:{"Chat with us!":"Chat med oss!","Scroll down to see new messages":"Bla ned for å se nye meldinger",Online:"Pålogget",Offline:"Avlogget",Connecting:"Koble til","Connection re-established":"Tilkoblingen er gjenopprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat avsluttes om %s","Compose your message...":"Skriv din melding...","All colleagues are busy.":"Alle våre kolleger er for øyeblikket opptatt.","You are on waiting list position %s.":"Du står nå i kø og er nr. %s på ventelisten.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med %s nå avsluttes.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!"},nb:{"Chat with us!":"Chat med oss!","Scroll down to see new messages":"Bla ned for å se nye meldinger",Online:"Pålogget",Offline:"Avlogget",Connecting:"Koble til","Connection re-established":"Tilkoblingen er gjenopprettet",Today:"I dag",Send:"Send","Chat closed by %s":"Chat avsluttes om %s","Compose your message...":"Skriv din melding...","All colleagues are busy.":"Alle våre kolleger er for øyeblikket opptatt.","You are on waiting list position %s.":"Du står nå i kø og er nr. %s på ventelisten.","Start new conversation":"Start en ny samtale","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene av samtalen, vil samtalen med %s nå avsluttes.","Since you didn't respond in the last %s minutes your conversation got closed.":"Ettersom du ikke har respondert i løpet av de siste %s minuttene, har samtalen nå blitt avsluttet.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Vi beklager, men det tar lengre tid enn vanlig å få en ledig plass i vår chat. Vennligst prøv igjen på et senere tidspunkt eller send oss en e-post. Tusen takk!"},el:{"Chat with us!":"Επικοινωνήστε μαζί μας!","Scroll down to see new messages":"Μεταβείτε κάτω για να δείτε τα νέα μηνύματα",Online:"Σε σύνδεση",Offline:"Αποσυνδεμένος",Connecting:"Σύνδεση","Connection re-established":"Η σύνδεση αποκαταστάθηκε",Today:"Σήμερα",Send:"Αποστολή","Chat closed by %s":"Η συνομιλία έκλεισε από τον/την %s","Compose your message...":"Γράψτε το μήνυμα σας...","All colleagues are busy.":"Όλοι οι συνάδελφοι μας είναι απασχολημένοι.","You are on waiting list position %s.":"Βρίσκεστε σε λίστα αναμονής στη θέση %s.","Start new conversation":"Έναρξη νέας συνομιλίας","Since you didn't respond in the last %s minutes your conversation with %s got closed.":"Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας με τον/την %s έκλεισε.","Since you didn't respond in the last %s minutes your conversation got closed.":"Από τη στιγμή που δεν απαντήσατε τα τελευταία %s λεπτά η συνομιλία σας έκλεισε.","We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!":"Λυπούμαστε που χρειάζεται περισσότερος χρόνος από τον αναμενόμενο για να βρεθεί μία κενή θέση. Παρακαλούμε δοκιμάστε ξανά αργότερα ή στείλτε μας ένα email. Ευχαριστούμε!"}},t.prototype.sessionId=void 0,t.prototype.scrolledToBottom=!0,t.prototype.scrollSnapTolerance=10,t.prototype.richTextFormatKey={66:!0,73:!0,85:!0,83:!0},t.prototype.T=function(){var e,t,n,o,s,i;if(s=arguments[0],t=2<=arguments.length?slice.call(arguments,1):[],this.options.lang&&"en"!==this.options.lang&&(this.translations[this.options.lang]?((i=this.translations[this.options.lang])[s]||this.log.notice("Translation needed for '"+s+"'"),s=i[s]||s):this.log.notice("Translation '"+this.options.lang+"' needed!")),t)for(n=0,o=t.length;n',document.execCommand("insertHTML",!1,s)},m.resizeImage(s.src,460,"auto",2,"image/jpeg","auto",t)},d.readAsDataURL(i),a=!0)),!a){o=h=void 0;try{h=n.getData("text/html"),o="html",h&&0!==h.length||(o="text",h=n.getData("text/plain")),h&&0!==h.length||(o="text2",h=n.getData("text"))}catch(e){t=e,console.log("Sorry, can't insert markup because browser is not supporting it."),o="text3",h=n.getData("text")}return"text"!==o&&"text2"!==o&&"text3"!==o||(h=(h="
                  "+h.replace(/\n/g,"
                  ")+"
                  ").replace(/
                  <\/div>/g,"

                  ")),console.log("p",o,h),"html"===o&&(u=DOMPurify.sanitize(h),m.log.debug("sanitized HTML clipboard",u),e=E("
                  "+u+"
                  "),l=!1,s=h,c=new RegExp("<(/w|w):[A-Za-z]"),s.match(c)&&(l=!0,s=s.replace(c,"")),c=new RegExp("<(/o|o):[A-Za-z]"),s.match(c)&&(l=!0,s=s.replace(c,"")),l&&(e=m.wordFilter(e)),(e=E(e)).contents().each(function(){if(8===this.nodeType)return E(this).remove()}),e.find("a, font, small, time, form, label").replaceWith(function(){return E(this).contents()}),e.find("textarea").each(function(){var e,t;return t=this.outerHTML,c=new RegExp("<"+this.tagName,"i"),e=t.replace(c,"')).get(0),document.caretPositionFromPoint?(s=document.caretPositionFromPoint(l,d),(i=document.createRange()).setStart(s.offsetNode,s.offset),i.collapse(),i.insertNode(a)):document.caretRangeFromPoint?(i=document.caretRangeFromPoint(l,d)).insertNode(a):console.log("could not find carat")},c.resizeImage(a.src,460,"auto",2,"image/jpeg","auto",t)},o.readAsDataURL(n)})),E(_).on("beforeunload",(e=this,function(){return e.onLeaveTemporary()})),E(_).bind("hashchange",(t=this,function(){if(!t.isOpen)return t.idleTimeout.start();t.sessionId&&t.send("chat_session_notice",{session_id:t.sessionId,message:_.location.href})})),this.isFullscreen)return this.input.on({focus:this.onFocus,focusout:this.onFocusOut})},t.prototype.stopPropagation=function(e){return e.stopPropagation()},t.prototype.checkForEnter=function(e){if(!this.inputDisabled&&!e.shiftKey&&13===e.keyCode)return e.preventDefault(),this.sendMessage()},t.prototype.send=function(e,t){return null==t&&(t={}),t.chat_id=this.options.chatId,this.io.send(e,t)},t.prototype.onWebSocketMessage=function(e){var t,n,o;for(t=0,n=e.length;tnew Date((new Date).getTime()-1500)))return this.isTyping=new Date,this.send("chat_session_typing",{session_id:this.sessionId}),this.inactiveTimeout.start()},t.prototype.onSubmit=function(e){return e.preventDefault(),this.sendMessage()},t.prototype.sendMessage=function(){var e,t;if(e=this.input.html())return this.inactiveTimeout.start(),sessionStorage.removeItem("unfinished_message"),t=this.view("message")({message:e,from:"customer",id:this._messageCount++,unreadClass:""}),this.maybeAddTimestamp(),this.el.find(".zammad-chat-message--typing").get(0)?(this.lastAddedType="typing-placeholder",this.el.find(".zammad-chat-message--typing").before(t)):(this.lastAddedType="message--customer",this.el.find(".zammad-chat-body").append(t)),this.input.html(""),this.scrollToBottom(),this.send("chat_session_message",{content:e,id:this._messageCount,session_id:this.sessionId})},t.prototype.receiveMessage=function(e){return this.inactiveTimeout.start(),this.onAgentTypingEnd(),this.maybeAddTimestamp(),this.renderMessage({message:e.message.content,id:e.id,from:"agent"}),this.scrollToBottom({showHint:!0})},t.prototype.renderMessage=function(e){return this.lastAddedType="message--"+e.from,e.unreadClass=document.hidden?" zammad-chat-message--unread":"",this.el.find(".zammad-chat-body").append(this.view("message")(e))},t.prototype.open=function(){var e;if(!this.isOpen)return this.isOpen=!0,this.log.debug("open widget"),this.show(),this.sessionId||this.showLoader(),this.el.addClass("zammad-chat-is-open"),e=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.css("bottom",-e),this.sessionId?(this.el.css("bottom",0),this.onOpenAnimationEnd()):(this.el.animate({bottom:0},500,this.onOpenAnimationEnd),this.send("chat_session_init",{url:_.location.href}));this.log.debug("widget already open, block")},t.prototype.onOpenAnimationEnd=function(){var e;return this.idleTimeout.stop(),this.isFullscreen&&this.disableScrollOnRoot(),"function"==typeof(e=this.options).onOpenAnimationEnd?e.onOpenAnimationEnd():void 0},t.prototype.sessionClose=function(){return this.send("chat_session_close",{session_id:this.sessionId}),this.inactiveTimeout.stop(),this.waitingListTimeout.stop(),sessionStorage.removeItem("unfinished_message"),this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.setSessionId(void 0)},t.prototype.toggle=function(e){return this.isOpen?this.close(e):this.open(e)},t.prototype.close=function(e){var t;if(this.isOpen)return this.initDelayId&&clearTimeout(this.initDelayId),this.sessionId&&(this.log.debug("session close before widget close"),this.sessionClose()),this.log.debug("close widget"),e&&e.stopPropagation(),this.isFullscreen&&this.enableScrollOnRoot(),t=this.el.height()-this.el.find(".zammad-chat-header").outerHeight(),this.el.animate({bottom:-t},500,this.onCloseAnimationEnd);this.log.debug("can't close widget, it's not open")},t.prototype.onCloseAnimationEnd=function(){var e;return this.el.css("bottom",""),this.el.removeClass("zammad-chat-is-open"),this.showLoader(),this.el.find(".zammad-chat-welcome").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").addClass("zammad-chat-is-hidden"),this.isOpen=!1,"function"==typeof(e=this.options).onCloseAnimationEnd&&e.onCloseAnimationEnd(),this.io.reconnect()},t.prototype.onWebSocketClose=function(){if(!this.isOpen)return this.el?(this.el.removeClass("zammad-chat-is-shown"),this.el.removeClass("zammad-chat-is-loaded")):void 0},t.prototype.show=function(){if("offline"!==this.state)return this.el.addClass("zammad-chat-is-loaded"),this.el.addClass("zammad-chat-is-shown")},t.prototype.disableInput=function(){return this.inputDisabled=!0,this.input.prop("contenteditable",!1),this.el.find(".zammad-chat-send").prop("disabled",!0),this.io.close()},t.prototype.enableInput=function(){return this.inputDisabled=!1,this.input.prop("contenteditable",!0),this.el.find(".zammad-chat-send").prop("disabled",!1)},t.prototype.hideModal=function(){return this.el.find(".zammad-chat-modal").html("")},t.prototype.onQueueScreen=function(e){var t,n;if(this.setSessionId(e.session_id),t=function(){return n.onQueue(e),n.waitingListTimeout.start()},!(n=this).initialQueueDelay||this.onInitialQueueDelayId)return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),t();this.onInitialQueueDelayId=setTimeout(t,this.initialQueueDelay)},t.prototype.onQueue=function(e){return this.log.notice("onQueue",e.position),this.inQueue=!0,this.el.find(".zammad-chat-modal").html(this.view("waiting")({position:e.position}))},t.prototype.onAgentTypingStart=function(){if(this.stopTypingId&&clearTimeout(this.stopTypingId),this.stopTypingId=setTimeout(this.onAgentTypingEnd,3e3),!this.el.find(".zammad-chat-message--typing").get(0)&&(this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("typingIndicator")()),this.isVisible(this.el.find(".zammad-chat-message--typing"),!0)))return this.scrollToBottom()},t.prototype.onAgentTypingEnd=function(){return this.el.find(".zammad-chat-message--typing").remove()},t.prototype.onLeaveTemporary=function(){if(this.sessionId)return this.send("chat_session_leave_temporary",{session_id:this.sessionId})},t.prototype.maybeAddTimestamp=function(){var e,t,n;if(n=Date.now(),!this.lastTimestamp||n-this.lastTimestamp>6e4*this.showTimeEveryXMinutes)return e=this.T("Today"),t=(new Date).toTimeString().substr(0,5),"timestamp"===this.lastAddedType?(this.updateLastTimestamp(e,t),this.lastTimestamp=n):(this.el.find(".zammad-chat-body").append(this.view("timestamp")({label:e,time:t})),this.lastTimestamp=n,this.lastAddedType="timestamp",this.scrollToBottom())},t.prototype.updateLastTimestamp=function(e,t){if(this.el)return this.el.find(".zammad-chat-body").find(".zammad-chat-timestamp").last().replaceWith(this.view("timestamp")({label:e,time:t}))},t.prototype.addStatus=function(e){if(this.el)return this.maybeAddTimestamp(),this.el.find(".zammad-chat-body").append(this.view("status")({status:e})),this.scrollToBottom()},t.prototype.detectScrolledtoBottom=function(){var e;if(e=this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-chat-body").outerHeight(),this.scrolledToBottom=Math.abs(e-this.el.find(".zammad-chat-body").prop("scrollHeight"))<=this.scrollSnapTolerance,this.scrolledToBottom)return this.el.find(".zammad-scroll-hint").addClass("is-hidden")},t.prototype.showScrollHint=function(){return this.el.find(".zammad-scroll-hint").removeClass("is-hidden"),this.el.find(".zammad-chat-body").scrollTop(this.el.find(".zammad-chat-body").scrollTop()+this.el.find(".zammad-scroll-hint").outerHeight())},t.prototype.onScrollHintClick=function(){return this.el.find(".zammad-chat-body").animate({scrollTop:this.el.find(".zammad-chat-body").prop("scrollHeight")},300)},t.prototype.scrollToBottom=function(e){var t;return t=(null!=e?e:{showHint:!1}).showHint,this.scrolledToBottom?this.el.find(".zammad-chat-body").scrollTop(E(".zammad-chat-body").prop("scrollHeight")):t?this.showScrollHint():void 0},t.prototype.destroy=function(e){return null==e&&(e={}),this.log.debug("destroy widget",e),this.setAgentOnlineState("offline"),e.remove&&this.el&&(this.el.remove(),E("."+this.options.buttonClass).hide()),this.waitingListTimeout&&this.waitingListTimeout.stop(),this.inactiveTimeout&&this.inactiveTimeout.stop(),this.idleTimeout&&this.idleTimeout.stop(),this.io.close()},t.prototype.reconnect=function(){return this.log.notice("reconnecting"),this.disableInput(),this.lastAddedType="status",this.setAgentOnlineState("connecting"),this.addStatus(this.T("Connection lost"))},t.prototype.onConnectionReestablished=function(){var e;return this.lastAddedType="status",this.setAgentOnlineState("online"),this.addStatus(this.T("Connection re-established")),"function"==typeof(e=this.options).onConnectionReestablished?e.onConnectionReestablished():void 0},t.prototype.onSessionClosed=function(e){var t;return this.addStatus(this.T("Chat closed by %s",e.realname)),this.disableInput(),this.setAgentOnlineState("offline"),this.inactiveTimeout.stop(),"function"==typeof(t=this.options).onSessionClosed?t.onSessionClosed(e):void 0},t.prototype.setSessionId=function(e){return void 0===(this.sessionId=e)?sessionStorage.removeItem("sessionId"):sessionStorage.setItem("sessionId",e)},t.prototype.onConnectionEstablished=function(e){var t;return this.onInitialQueueDelayId&&clearTimeout(this.onInitialQueueDelayId),this.inQueue=!1,e.agent&&(this.agent=e.agent),e.session_id&&this.setSessionId(e.session_id),this.el.find(".zammad-chat-body").html(""),this.el.find(".zammad-chat-agent").html(this.view("agent")({agent:this.agent})),this.enableInput(),this.hideModal(),this.el.find(".zammad-chat-welcome").addClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent").removeClass("zammad-chat-is-hidden"),this.el.find(".zammad-chat-agent-status").removeClass("zammad-chat-is-hidden"),this.isFullscreen||this.input.focus(),this.setAgentOnlineState("online"),this.waitingListTimeout.stop(),this.idleTimeout.stop(),this.inactiveTimeout.start(),"function"==typeof(t=this.options).onConnectionEstablished?t.onConnectionEstablished(e):void 0},t.prototype.showCustomerTimeout=function(){var e;return this.el.find(".zammad-chat-modal").html(this.view("customer_timeout")({agent:this.agent.name,delay:this.options.inactiveTimeout})),e=function(){return location.reload()},this.el.find(".js-restart").click(e),this.sessionClose()},t.prototype.showWaitingListTimeout=function(){var e;return this.el.find(".zammad-chat-modal").html(this.view("waiting_list_timeout")({delay:this.options.watingListTimeout})),e=function(){return location.reload()},this.el.find(".js-restart").click(e),this.sessionClose()},t.prototype.showLoader=function(){return this.el.find(".zammad-chat-modal").html(this.view("loader")())},t.prototype.setAgentOnlineState=function(e){var t;if(this.state=e,this.el)return t=e.charAt(0).toUpperCase()+e.slice(1),this.el.find(".zammad-chat-agent-status").attr("data-status",e).text(this.T(t))},t.prototype.detectHost=function(){var e;return e="ws://","https"===r&&(e="wss://"),this.options.host=""+e+a+"/ws"},t.prototype.loadCss=function(){var e,t,n;if(this.options.cssAutoload)return(n=this.options.cssUrl)||(n=this.options.host.replace(/^wss/i,"https").replace(/^ws/i,"http").replace(/\/ws$/i,""),n+="/assets/chat/chat.css"),this.log.debug("load css from '"+n+"'"),t="@import url('"+n+"');",(e=document.createElement("link")).onload=this.onCssLoaded,e.rel="stylesheet",e.href="data:text/css,"+escape(t),document.getElementsByTagName("head")[0].appendChild(e)},t.prototype.onCssLoaded=function(){var e;return this.cssLoaded=!0,this.socketReady&&this.onReady(),"function"==typeof(e=this.options).onCssLoaded?e.onCssLoaded():void 0},t.prototype.startTimeoutObservers=function(){var e,t,n;return this.idleTimeout=new s({logPrefix:"idleTimeout",debug:this.options.debug,timeout:this.options.idleTimeout,timeoutIntervallCheck:this.options.idleTimeoutIntervallCheck,callback:(e=this,function(){return e.log.debug("Idle timeout reached, hide widget",new Date),e.destroy({remove:!0})})}),this.inactiveTimeout=new s({logPrefix:"inactiveTimeout",debug:this.options.debug,timeout:this.options.inactiveTimeout,timeoutIntervallCheck:this.options.inactiveTimeoutIntervallCheck,callback:(t=this,function(){return t.log.debug("Inactive timeout reached, show timeout screen.",new Date),t.showCustomerTimeout(),t.destroy({remove:!1})})}),this.waitingListTimeout=new s({logPrefix:"waitingListTimeout",debug:this.options.debug,timeout:this.options.waitingListTimeout,timeoutIntervallCheck:this.options.waitingListTimeoutIntervallCheck,callback:(n=this,function(){return n.log.debug("Waiting list timeout reached, show timeout screen.",new Date),n.showWaitingListTimeout(),n.destroy({remove:!1})})})},t.prototype.disableScrollOnRoot=function(){return this.rootScrollOffset=this.scrollRoot.scrollTop(),this.scrollRoot.css({overflow:"hidden",position:"fixed"})},t.prototype.enableScrollOnRoot=function(){return this.scrollRoot.scrollTop(this.rootScrollOffset),this.scrollRoot.css({overflow:"",position:""})},t.prototype.isVisible=function(e,t,n,o){var s,i,a,r,l,d,c,u,h,m,p,g,f,y,v,b,w,T,C,S,k,z,A,x,O,I;if(!(e.length<1))if(i=E(_),T=(s=1/gi,"")).replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,"")).replace(/<(\/?)s>/gi,"<$1strike>")).replace(/ /gi," "),e.html(t),E("p",e).each(function(){var e,t;if(t=E(this).attr("style"),e=/mso-list:\w+ \w+([0-9]+)/.exec(t))return E(this).data("_listLevel",parseInt(e[1],10))}),c=0,u=null,E("p",e).each(function(){var e,t,n,o,s,i,a,r,l,d;if(void 0!==(e=E(this).data("_listLevel"))){if(d=E(this).text(),o="
                    ",/^\s*\w+\./.test(d)&&(o=(s=/([0-9])\./.exec(d))?null!=(i=1<(l=parseInt(s[1],10)))?i:'
                      ':"
                        "}:"
                          "),c"+E(this).html()+""),E(this).remove(),c=e}return c=0}),E("[style]",e).removeAttr("style"),E("[align]",e).removeAttr("align"),E("span",e).replaceWith(function(){return E(this).contents()}),E("span:empty",e).remove(),E("[class^='Mso']",e).removeAttr("class"),E("p:empty",e).remove(),e},t.prototype.removeAttribute=function(e){var t,n,o,s,i;if(e){for(t=E(e),o=0,s=(i=e.attributes).length;o/g,">").replace(/"/g,""")}),function(){(function(){t.push('
                          \n
                          \n
                          \n \n \n \n \n \n
                          \n
                          \n
                          \n
                          \n \n '),t.push(this.T(this.title)),t.push('\n
                          \n
                          \n
                          \n \n
                          \n
                          \n
                          \n \n
                          \n
                          ")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.customer_timeout=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
                          \n '),this.agent?(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation with %s got closed.",this.delay,this.agent))):(t.push("\n "),t.push(this.T("Since you didn't respond in the last %s minutes your conversation got closed.",this.delay))),t.push("\n "),t.push('\n
                          \n
                          "),t.push(this.T("Start new conversation")),t.push("
                          \n
                          ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.loader=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('\n \n \n \n\n'),t.push(this.T("Connecting")),t.push("")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.message=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?s(e):""},o=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                          \n "),t.push(this.message),t.push("\n
                          ")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.status=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                          \n
                          \n '),t.push(this.status),t.push("\n
                          \n
                          ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.timestamp=function(e){e||(e={});var t=[],n=function(e){return e&&e.ecoSafe?e:void 0!==e&&null!=e?s(e):""},o=e.safe,s=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},s||(s=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                          '),t.push(n(this.label)),t.push(" "),t.push(n(this.time)),t.push("
                          ")}).call(this)}.call(e),e.safe=o,e.escape=s,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.typingIndicator=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                          \n \n \n \n \n \n \n \n
                          ')}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){t.push('
                          \n \n \n \n \n \n '),t.push(this.T("All colleagues are busy.")),t.push("
                          \n "),t.push(this.T("You are on waiting list position %s.",this.position)),t.push("\n
                          ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")},window.zammadChatTemplates||(window.zammadChatTemplates={}),window.zammadChatTemplates.waiting_list_timeout=function(e){e||(e={});var t=[],n=e.safe,o=e.escape;return e.safe=function(e){if(e&&e.ecoSafe)return e;void 0!==e&&null!=e||(e="");var t=new String(e);return t.ecoSafe=!0,t},o||(o=e.escape=function(e){return(""+e).replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}),function(){(function(){var e;t.push('
                          \n '),t.push(this.T("We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!")),t.push('\n
                          \n
                          "),t.push(this.T("Start new conversation")),t.push("
                          \n
                          ")}).call(this)}.call(e),e.safe=n,e.escape=o,t.join("")}; \ No newline at end of file diff --git a/public/assets/tests/form.js b/public/assets/tests/form.js index 3eb0468d9..a28d4a4cc 100644 --- a/public/assets/tests/form.js +++ b/public/assets/tests/form.js @@ -1125,7 +1125,7 @@ test("object manager form 1", function() { params = App.ControllerForm.params(el) var test_params = { data_option: { - diff: 24, + diff: null, future: true, past: true }, @@ -1607,3 +1607,27 @@ test("form with external links", function() { equal('https://example.com/?q=133', el.find('input[name="a"]').parents('.controls').find('a[href]').attr('href')) equal('https://example.com/?q=abc%20d', el.find('select[name="b"]').parents('.controls').find('a[href]').attr('href')) }); + +QUnit.test("Fixes #3909 - Wrong size for textareas in triggers and core workflow.", assert => { + var done = assert.async(1) + $('#qunit').append('

                          Fixes #3909 - Wrong size for textareas in triggers and core workflow.

                          ') + var el = $('#form21') + new App.ControllerForm({ + el: el, + model: { + configure_attributes: [ + { name: '3909_textarea_expanding', display: 'Textarea1', tag: 'textarea', rows: 6, limit: 100, null: true }, + { name: '3909_textarea_no_expanding', display: 'Textarea2', tag: 'textarea', rows: 6, limit: 100, null: false, expanding: false }, + ], + }, + autofocus: true + }); + + new Promise( (resolve, reject) => { + App.Delay.set(resolve, 200); + }).then( function() { + assert.equal(el.find('textarea[name="3909_textarea_expanding"]').parent().hasClass('expanding-wrapper'), true, '3909_textarea_expanding has correct class') + assert.equal(el.find('textarea[name="3909_textarea_no_expanding"]').parent().hasClass('expanding-wrapper'), false, '3909_textarea_no_expanding has correct class') + }) + .finally(done) +}); diff --git a/public/assets/tests/form_extended.js b/public/assets/tests/form_extended.js index b4ce7be80..1ebc50a60 100644 --- a/public/assets/tests/form_extended.js +++ b/public/assets/tests/form_extended.js @@ -170,12 +170,15 @@ test('form checks', function() { first_response_time: '150', first_response_time_enabled: 'on', first_response_time_in_text: '02:30', + response_time: '', + response_time_in_text: '', solution_time: '', solution_time_enabled: undefined, solution_time_in_text: '', update_time: '45', update_time_enabled: 'on', update_time_in_text: '00:45', + update_type: 'update', working_hours: { mon: { active: true, @@ -318,15 +321,90 @@ test('form checks', function() { first_response_time: '30', first_response_time_enabled: 'on', first_response_time_in_text: '00:30', + response_time: '', + response_time_in_text: '', solution_time: '', solution_time_enabled: undefined, solution_time_in_text: '', update_time: '', update_time_enabled: undefined, update_time_in_text: '', + update_type: undefined, } deepEqual(params, test_params, 'form param check') + // change sla times + el.find('#update_time').attr('checked', false) + el.find('[value=response]').click() + el.find('[name="response_time_in_text"]').val('4:30').trigger('blur') + + var params = App.ControllerForm.params(el) + var test_params = { + priority1_id: '1', + priority2_id: ['1', '2'], + priority3_id: '2', + priority4_id: '2', + priority5_id: '1', + working_hours: { + mon: { + active: true, + timeframes: [ + ['09:00','17:00'] + ] + }, + tue: { + active: true, + timeframes: [ + ['00:00','22:00'] + ] + }, + wed: { + active: true, + timeframes: [ + ['09:00','17:00'] + ] + }, + thu: { + active: true, + timeframes: [ + ['09:00','12:00'], + ['13:00','17:00'] + ] + }, + fri: { + active: true, + timeframes: [ + ['09:00','17:00'] + ] + }, + sat: { + active: false, + timeframes: [ + ['10:00','14:00'] + ] + }, + sun: { + active: false, + timeframes: [ + ['10:00','14:00'] + ] + }, + }, + first_response_time: '30', + first_response_time_enabled: 'on', + first_response_time_in_text: '00:30', + response_time: '270', + response_time_in_text: '04:30', + solution_time: '', + solution_time_enabled: undefined, + solution_time_in_text: '', + update_time: '', + update_time_enabled: 'on', + update_time_in_text: '', + update_type: 'response' + } + deepEqual(params, test_params, 'form param check post response') + /* empty params or defaults */ $('#forms').append('

                          form condition check

                          ') var el = $('#form2') diff --git a/public/assets/tests/form_sla_times.js b/public/assets/tests/form_sla_times.js index 0c1518ed0..57153d6a7 100644 --- a/public/assets/tests/form_sla_times.js +++ b/public/assets/tests/form_sla_times.js @@ -57,6 +57,30 @@ test("form SLA times highlights and shows settings accordingly", function(assert equal(firstRow.find('input[data-name=first_response_time]').val(), '') ok(secondRow.hasClass('is-active')) equal(secondRow.find('input[data-name=update_time]').val(), '04:00') + equal(secondRow.find('input[name=update_type]:checked').val(), 'update') + + $('#forms').append('

                          SLA with response time set

                          ') + + var el = $('#form3a') + + var item = new App.Sla() + item.id = '123' + item.response_time = 180 + + new App.ControllerForm({ + el: el, + model: item.constructor, + params: item + }); + + var firstRow = el.find('.sla_times tbody > tr:first') + var secondRow = el.find('.sla_times tbody > tr:nth-child(2)') + + notOk(firstRow.hasClass('is-active')) + equal(firstRow.find('input[data-name=first_response_time]').val(), '') + ok(secondRow.hasClass('is-active')) + equal(secondRow.find('input[data-name=response_time]').val(), '03:00') + equal(secondRow.find('input[name=update_type]:checked').val(), 'response') }) test("form SLA times clears field instead of 00:00", function(assert) { diff --git a/public/assets/tests/i18n.js b/public/assets/tests/i18n.js index 0b994235f..55b6c724a 100644 --- a/public/assets/tests/i18n.js +++ b/public/assets/tests/i18n.js @@ -70,6 +70,24 @@ test('i18n .detectBrowserLocale', function() { translated = App.i18n.translateInline('yes') equal(translated, 'ja', 'de-de - yes / ja translated correctly') + translated = App.i18n.translateDeep({ + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + today: 'today', + }) + deepEqual(translated, { + days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + today: 'Heute', + }, 'de-de - deep object/array translated correctly') + + translated = App.i18n.translateDeepPlain({ + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + today: 'today', + }) + deepEqual(translated, { + days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], + today: 'Heute', + }, 'de-de - deep object/array translated correctly') + translated = App.i18n.translateContent('%s ago', 123); equal(translated, 'vor 123', 'de-de - %s') diff --git a/public/assets/tests/model.js b/public/assets/tests/model.js index e823cdb1c..bd695d292 100644 --- a/public/assets/tests/model.js +++ b/public/assets/tests/model.js @@ -227,8 +227,9 @@ test("updateAttributes will change existing attributes and add new ones", functi var attributesAfterUpdate = _.clone(App.Ticket.configure_attributes); equal(attributesAfterUpdate.length, attributesBefore.length + 1, 'new attributes list contains 1 more elements') - equal(attributesAfterUpdate[attributesAfterUpdate.length - 1]['name'], 'new_attribute_1010101', 'new attributes list contains the new element') - equal(attributesAfterUpdate[0]['new_option_1239393'], 1, 'first element of the new attributes got updated with the new option') + equal(attributesAfterUpdate[0]['new_option_1239393'], 1, 'first element of the new attributes is number') + equal(attributesAfterUpdate[0]['name'], 'number', 'first element of the new attributes got updated with the new option') + equal(attributesAfterUpdate[1]['name'], 'new_attribute_1010101', 'new attributes list contains the new element') App.Ticket.resetAttributes(); var attributesAfterReset = _.clone(App.Ticket.configure_attributes); diff --git a/public/assets/tests/view_helpers.js b/public/assets/tests/view_helpers.js new file mode 100644 index 000000000..9ebf4385c --- /dev/null +++ b/public/assets/tests/view_helpers.js @@ -0,0 +1,25 @@ +QUnit.test("time_duration_hh_mm", assert => { + let func = App.ViewHelpers.time_duration_hh_mm + assert.equal(func(1), '00:01') + + assert.equal(func(61), '01:01') + + assert.equal(func(3600), '60:00') + + assert.equal(func(7200), '120:00') +}) + +QUnit.test("time_duration", assert => { + let func = App.ViewHelpers.time_duration + assert.equal(func(1), '00:01') + + assert.equal(func(61), '01:01') + + assert.equal(func(3600), '1:00:00') + + assert.equal(func(7200), '2:00:00') + + assert.equal(func(36000), '10:00:00') + + assert.equal(func(360101), '100:01:41') +}) diff --git a/spec/db/migrate/issue_3622_add_callback_url_spec.rb b/spec/db/migrate/issue_3622_add_callback_url_spec.rb new file mode 100644 index 000000000..6d6bbf84c --- /dev/null +++ b/spec/db/migrate/issue_3622_add_callback_url_spec.rb @@ -0,0 +1,23 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe Issue3622AddCallbackUrl, type: :db_migration do + let(:field) do + { + 'display' => 'Your callback URL', + 'null' => true, + 'name' => 'callback_url', + 'tag' => 'auth_provider', + 'provider' => 'auth_twitter' + } + end + + before do + migrate + end + + it 'does update settings correctly' do + expect(Setting.find_by(name: 'auth_twitter_credentials').options['form']).to include(field) + end +end diff --git a/spec/db/migrate/issue_3810_custom_date_attribute_no_default_spec.rb b/spec/db/migrate/issue_3810_custom_date_attribute_no_default_spec.rb new file mode 100644 index 000000000..46cefe948 --- /dev/null +++ b/spec/db/migrate/issue_3810_custom_date_attribute_no_default_spec.rb @@ -0,0 +1,21 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe Issue3810CustomDateAttributeNoDefault, type: :db_migration, db_strategy: :reset_all do + before :all do # rubocop:disable RSpec/BeforeAfterAll + create('object_manager_attribute_date', name: 'rspec_date', default: 24) + create('object_manager_attribute_datetime', name: 'rspec_datetime', default: 24) + + ObjectManager::Attribute.migration_execute + end + + after :all do # rubocop:disable RSpec/BeforeAfterAll + ObjectManager::Attribute.where('name LIKE ?', 'rspec_%').destroy_all + end + + it 'unsets diff migration' do + migrate + expect(create(:ticket)).to have_attributes(rspec_date: nil, rspec_datetime: nil) + end +end diff --git a/spec/db/migrate/issue_3851_spec.rb b/spec/db/migrate/issue_3851_spec.rb new file mode 100644 index 000000000..683536e9f --- /dev/null +++ b/spec/db/migrate/issue_3851_spec.rb @@ -0,0 +1,32 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe Issue3851, type: :db_migration do + let(:follow_up_assignment) { ObjectManager::Attribute.for_object('Group').find_by(name: 'follow_up_assignment') } + let(:follow_up_possible) { ObjectManager::Attribute.for_object('Group').find_by(name: 'follow_up_possible') } + + before do + migrate + end + + it 'shows field follow_up_assignment with correct default' do + expect(follow_up_assignment.data_option['default']).to eq('true') + end + + it 'shows field follow_up_assignment required in create' do + expect(follow_up_assignment.screens['create']['-all-']['null']).to eq(false) + end + + it 'shows field follow_up_assignment required in edit' do + expect(follow_up_assignment.screens['edit']['-all-']['null']).to eq(false) + end + + it 'shows field follow_up_possible required in create' do + expect(follow_up_possible.screens['create']['-all-']['null']).to eq(false) + end + + it 'shows field follow_up_possible required in edit' do + expect(follow_up_possible.screens['edit']['-all-']['null']).to eq(false) + end +end diff --git a/spec/db/migrate/maintenance_remove_active_ldap_sessions_spec.rb b/spec/db/migrate/maintenance_remove_active_ldap_sessions_spec.rb new file mode 100644 index 000000000..0d4a81bff --- /dev/null +++ b/spec/db/migrate/maintenance_remove_active_ldap_sessions_spec.rb @@ -0,0 +1,30 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe MaintenanceRemoveActiveLdapSessions, type: :db_migration do + before do + 5.times do + ActiveRecord::SessionStore::Session.create( + session_id: SecureRandom.hex(16), + data: SecureRandom.base64(10) + ) + end + end + + context 'without ldap integration' do + before { Setting.set('ldap_integration', false) } + + it 'does not delete existing sessions' do + expect { migrate }.not_to change(ActiveRecord::SessionStore::Session, :count) + end + end + + context 'with ldap integration' do + before { Setting.set('ldap_integration', true) } + + it 'deletes all existing sessions' do + expect { migrate }.to change(ActiveRecord::SessionStore::Session, :count).to(0) + end + end +end diff --git a/spec/factories/object_manager_attribute.rb b/spec/factories/object_manager_attribute.rb index 02732b88e..f7847ed50 100644 --- a/spec/factories/object_manager_attribute.rb +++ b/spec/factories/object_manager_attribute.rb @@ -41,6 +41,8 @@ FactoryBot.define do end factory :object_manager_attribute_text, parent: :object_manager_attribute do + default { '' } + data_type { 'input' } data_option do { @@ -48,7 +50,7 @@ FactoryBot.define do 'maxlength' => 200, 'null' => true, 'translate' => false, - 'default' => default || '', + 'default' => default, 'options' => {}, 'relation' => '', } @@ -56,10 +58,12 @@ FactoryBot.define do end factory :object_manager_attribute_integer, parent: :object_manager_attribute do + default { 0 } + data_type { 'integer' } data_option do { - 'default' => default || 0, + 'default' => default, 'min' => 0, 'max' => 9999, } @@ -67,10 +71,12 @@ FactoryBot.define do end factory :object_manager_attribute_boolean, parent: :object_manager_attribute do + default { false } + data_type { 'boolean' } data_option do { - default: default || false, + default: default, options: { true => 'yes', false => 'no', @@ -80,34 +86,40 @@ FactoryBot.define do end factory :object_manager_attribute_date, parent: :object_manager_attribute do + default { 24 } + name { 'date_attribute' } data_type { 'date' } data_option do { - 'diff' => default || 24, + 'diff' => default, 'null' => true, } end end factory :object_manager_attribute_datetime, parent: :object_manager_attribute do + default { 24 } + name { 'datetime_attribute' } data_type { 'datetime' } data_option do { 'future' => true, 'past' => true, - 'diff' => default || 24, + 'diff' => default, 'null' => true, } end end factory :object_manager_attribute_select, parent: :object_manager_attribute do + default { '' } + data_type { 'select' } data_option do { - 'default' => default || '', + 'default' => default, 'options' => { 'key_1' => 'value_1', 'key_2' => 'value_2', @@ -124,6 +136,8 @@ FactoryBot.define do end factory :object_manager_attribute_tree_select, parent: :object_manager_attribute do + default { '' } + data_type { 'tree_select' } data_option do { diff --git a/spec/factories/report/profile.rb b/spec/factories/report/profile.rb index a416752b6..b9f46e18d 100644 --- a/spec/factories/report/profile.rb +++ b/spec/factories/report/profile.rb @@ -6,5 +6,20 @@ FactoryBot.define do active { true } created_by_id { 1 } updated_by_id { 1 } + + trait :condition_created_at do + transient do + ticket_created_at { nil } + end + + condition do + { + 'ticket.created_at' => { + operator: 'before (absolute)', + value: ticket_created_at.iso8601 + } + } + end + end end end diff --git a/spec/factories/user.rb b/spec/factories/user.rb index 14edb9906..cf7af9266 100644 --- a/spec/factories/user.rb +++ b/spec/factories/user.rb @@ -58,6 +58,18 @@ FactoryBot.define do user.define_singleton_method(:password_plain, -> { password_plain }) end + trait :groupable do + transient do + group { nil } + end + + after(:create) do |user, context| + Array(context.group).each do |group| + user.groups << group + end + end + end + trait :preferencable do transient do notification_group_ids { [] } @@ -77,6 +89,17 @@ FactoryBot.define do } end end + + trait :ooo do + transient do + ooo_agent { nil } + end + + out_of_office { true } + out_of_office_start_at { 1.day.ago } + out_of_office_end_at { 1.day.from_now } + out_of_office_replacement_id { ooo_agent.id } + end end sequence(:password_valid) do |n| diff --git a/spec/fixtures/smime/issue_3727.key b/spec/fixtures/smime/issue_3727.key new file mode 100644 index 000000000..2609a610c --- /dev/null +++ b/spec/fixtures/smime/issue_3727.key @@ -0,0 +1,87 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEA5Uos3YnMM4DLyO57KSshsUFC71+OqGdmVdWRln5P5R8IFR1D +9clNsz4o/SfJQVsI2WjnHtJs/2y3GMilTm56tOChBiwY0EiB7icy5t+BTqXigZJf +3PtK2zpHe6wU6w3mO2h1DLCP+dwuiCSYLkq+Gvdb+Q3xs6O3rDw+uX868MTwMde6 +Id6q5bq8po8PCdqrSLNBXBas5k8ixsMzKTDwv4DzPdh4xmml+4qfOC1GgC+27iSc +RJGNYpfXqJzoMdpXF379YAEEbHaLWr5GXQ7uHUlNiaE0fFv8pcm5OONQDn88KzKx +/24X31zyIcnAacwHyVQ1ueqP9VDK5zHV3vVVHWKqhoQC/YRy8CnjHim9NbL0pFyk +BrYbVa49bsZ39pRmljTSRxv8osoOn5NPqOnuR7EjTyOdltJvXs8R4IB3TTqNchf7 +2MemJRPgyn0Dj1NOHaJ+9K1w73vy3CNhTb3VijHTT0zgJuAuaxgjD7EHL0VFnVIl +yX2kaXquhBIi1naZPA1a1acXlxYrbAWqf97BF5B95Pi0afIHdOqeFS3RD3Fpj4jK +i2HdAEzYBA7r56qpsSJNxW0eBxqUlTu4g+B37fRijyZXAW74a2yS1B530OTEzzvv +GblaaM1pb4LoeT0dvqnoq7/0J2C+ZkeWXgTH3oobcc73CdROhszSJUHmT+kCAwEA +AQKCAgA+C3JUiGMvVJzQRGgjXb6CPoykRZFO1JwGggIhXRC1iU4gmIi5S72w0RM6 +XbfB7aZZXl+cIYjJHVv2YuUIcjDWHSq1ht04D0bJcOX/P1+4Ln86XKeAHqfE5uJM +/uWyLVKtpLh3tJdhH0mgIXbkn+kNVv4WSMWsvJKJEsxOWbVTTZdJhXyiiaRpAbzm +vTNukTNkOs1m4+PpdmSMsGl5rfqXd4dapucXmaMGjB5Fj0rSiRbRHisDCvfdRAVh +ZQQX6WNDwmNBxUSzLOjMp0xXBiE834cRxQN021dkbU+nqysQoTFg5xjva5UeJgKH +Tha5CjLZMeZP5r8JvNEK+ptK98wN4nLDqTlvgFr/L7RSrlj6KUWnM8e+cOMo/2T9 +VQ/7XQhZ0lxvVr9xEW5pkKqlq4Tyd4YwniU/rNNJV7cyqacpDSN4go/kYFsLCb5F +wY8CIBg/jLM4a7i7nt7UXcNVh3iRvhq8CduG4paQeJbxTSmzge4VAFwKO0TXotvN +V2Um+ibFc7S/9Tzy1BeTfUUE9nAWP9sXomBBXwG59Au9graorzuAUv+y2XfzTqK+ +QNtrHzL0VCODXAjOoSTr2Fhic+VR4lpxpZAzHxaOVKMHNW9+Z/QEFdgEjC4dvWXc +7geWNcrzjspz918j1CKihaVFX1tamDPSb1gk1+kgsDrwy3lI+QKCAQEA9KAxZ35/ ++bJbYXFzQFlaFyBjny5VGzchfhydj66S01NofmQLdyGHcWwqVf1DdL8dWmJC0eaP +pkIzLj/F6uvowASUk80MNARkVLMRr0M2y1p+MkLHy2eabaHF8S8AuaHV1bZtchCw +L00uPM5XECFqzctJv2Rcx+TAgDz0DfLDH/+Z4qhxYP93a/4aQnEMCrLPB8lJtGu9 +dzilzm7UKHddTSudX5N9XHQmwLe6lsaXaJ8F5wocRyw6/Ay3yesluNRAZ2BYGDfc +8Vh3tV9bfw8e1v8xgT3KyHSBo5kKxlHmQATBDTV490sLmK8U37my4UYeZYST4ag3 +4CnX1E5A0RhVxwKCAQEA7/NvpOOGCHqL6NDK1VvEP5axX9iS9rUPHAi6bxhf9RrR +K6S+9iVHgaOZMoVguHHqYfJUO3UPZlZ7h2gGppwgp/he2743mSeURiHOK7c6JIsH +KJhCCCiUjJeonykLHvbMQV8Nf9Ci+j/N3A+pwiR+ogLJY8vPSS2Hx+X/LoF1MUX/ +54LJwYFM1T8yVrDNXY29QrtBFMN+usyZ2KFyEPukfLNI0yZl+XlpHWedWHqZWcXK +DNAgOBoe5LVaAUnKatrEZH9t0tKYax+DTaDz3eA5YW8s7pKOFEjRBrZnoBW/dYfz +oF3QzEDlOb9dS+rx6+tj/ifn9RDcLFsRZs7ieK9szwKCAQEAwraDvJI3QURTckOA +bjbw+7l/MmQJwAjo8t3KGGTnX6hjYz801RVuHrzvEdTujY3VymyuLS8tJjRJUsXW +PsCaWcULkn3C+eCJD9Yc/HkuszyLeGwpZeFITX1X9jrog9mqQFrd0M4xvuTbKfE/ +4YoH3liykdJL+5w8EZby1+tknyKvlXdoD8Ioh2AR/NLIt/dNzS/OJ/seKzh+2crj +unYQYO2XbU0TmrSlZ/6WWY8nU1JIu3cTvR8asCdbXzB5rR3dSaupU1Wb2ssFNev6 +Ay/A53bnK61IrLf3vIWDywnDkS93jpECgSxNxbGOlunT1XYfmcSmhRaFqzsDHW1Q +MF8DXwKCAQB+S/TEpllDFzWTCmromE+YZLnhx/26yxwz1khC92JygXX9cc5tgru7 +eZ/GHrwE+Tiz6zf4v6mmZPjKEbAGfAEYSDutj9Z1z4ZUz7BUBDIfT+oprNJ8ttdR +lPXVKGZJGv/xnJVfZDKUY4b4QGpK3Kimn67ez0TAsK1aQy3ojY1grQaAFbAaIPOO +/p+BT7gYeOVYPXWI90k6Cz0i7/84/yrZ1AgN05UzFXuFVadVDdqvjNLHobiDrwP5 +v5arPOrFCXb7qrLkl6JQKBsVfhU+AKpTJZBR1mPgO1+CF/o9IZVPyIosK5UeHT5K +AfaaYgSJ97D+8oQ90m0BD8H+CgDcIwGzAoIBAC4KznWcLMOs7IIWu0OIfK1VWHdc +dcKwJOs+jGM4SaGWDTBUdVU2NE43fyphwJjBmj+efogeRW1JRBJJ3FNE8YULDmxO +UJEYyYvmuzsWNoF6e0uqLmn+aO6m1kbVnkF/YA5MR3ovKy6YAIVXIhGKPv8SHfea +nyLMORaVy0R8o3fyq6FMKoEf1p+2Gx3mMeKOIghhB/7clHWGZO8igKab+juGZPiK +3vd9Mg00GugWQZpIoyOIQvbdtNARVOVRCWDdtrdyb5UsfZx7fJBWnDiGhXjsGDYU +7bg30nj69KxQTuWR64tY4bBrTIifLWUP4YRm//lljcyjwzZUNP63ujD2eHI= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIGTTCCBDWgAwIBAgIBKDANBgkqhkiG9w0BAQ0FADCBjjELMAkGA1UEBhMCREUx +DzANBgNVBAgTBkJlcmxpbjEPMA0GA1UEBxMGQmVybGluMRUwEwYDVQQKEwxTZWNy +ZXQgQ29ycC4xCzAJBgNVBAsTAklUMRMwEQYDVQQDEwpFeGFtcGxlLUNBMSQwIgYJ +KoZIhvcNAQkBFhVjYS1pc3N1ZXNAZXhhbXBsZS5jb20wHhcNMjEwOTA3MTQxNDAw +WhcNMzAwNjAyMTQwMDAwWjCBkzELMAkGA1UEBhMCREUxDzANBgNVBAgTBkJlcmxp +bjEQMA4GA1UEBxMHR2VybWFueTEUMBIGA1UEChMLWmFtbWFkIEdtYkgxCzAJBgNV +BAsTAklUMRswGQYDVQQDDBJzYW1wbGVAZXhhbXBsZS5jb20xITAfBgkqhkiG9w0B +CQEWEnNhbXBsZUBleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAOVKLN2JzDOAy8jueykrIbFBQu9fjqhnZlXVkZZ+T+UfCBUdQ/XJTbM+ +KP0nyUFbCNlo5x7SbP9stxjIpU5uerTgoQYsGNBIge4nMubfgU6l4oGSX9z7Sts6 +R3usFOsN5jtodQywj/ncLogkmC5Kvhr3W/kN8bOjt6w8Prl/OvDE8DHXuiHequW6 +vKaPDwnaq0izQVwWrOZPIsbDMykw8L+A8z3YeMZppfuKnzgtRoAvtu4knESRjWKX +16ic6DHaVxd+/WABBGx2i1q+Rl0O7h1JTYmhNHxb/KXJuTjjUA5/PCsysf9uF99c +8iHJwGnMB8lUNbnqj/VQyucx1d71VR1iqoaEAv2EcvAp4x4pvTWy9KRcpAa2G1Wu +PW7Gd/aUZpY00kcb/KLKDp+TT6jp7kexI08jnZbSb17PEeCAd006jXIX+9jHpiUT +4Mp9A49TTh2ifvStcO978twjYU291Yox009M4CbgLmsYIw+xBy9FRZ1SJcl9pGl6 +roQSItZ2mTwNWtWnF5cWK2wFqn/ewReQfeT4tGnyB3TqnhUt0Q9xaY+Iyoth3QBM +2AQO6+eqqbEiTcVtHgcalJU7uIPgd+30Yo8mVwFu+GtsktQed9DkxM877xm5WmjN +aW+C6Hk9Hb6p6Ku/9CdgvmZHll4Ex96KG3HO9wnUTobM0iVB5k/pAgMBAAGjga4w +gaswDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUeecOtMgq/dUjz9DSBv9Zv/z5gE8w +CwYDVR0PBAQDAgPoMB0GA1UdJQQWMBQGCCsGAQUFBwMDBggrBgEFBQcDBDAdBgNV +HREEFjAUgRJzYW1wbGVAZXhhbXBsZS5jb20wEQYJYIZIAYb4QgEBBAQDAgUgMB4G +CWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZIhvcNAQENBQADggIB +ALT3Mfzak7PHC1bmbHN58dhxaUdlhzX1u3UlDp7vPnm/7lKu4fw127qEQY186tni +Krn4bWYeYYo78pBmejzarkaA6UKOXtFC0IEepehNsCPcwjkxSp7FFqpjZ+krWwbU +wD8Ou3bXJVBsMvCZ/ucJc8ThOGqF4Lgeyvy4mw75WJtFe304fAhDTLedRyXqjhLX +4t0UT9cvKLfkqJCu051nDOlQs58stq63beUCZ1vIDu8jJNZ7PzT1F21AxMpYY6uq +0Cyb9qdMi3VNudbD62Ze85+qh4nunWIvBTGBZab3JhFvCRzafYiJwo6xT1+cFsiF +ZaUejtUPQNg0T6gOZIAu5tb1cgwBMBX0uD2gl7NPbqXsLET5U30a0AbGbM1p61H3 +EaBcl4MvtC4yUGfV/HrY6ZtzzYaKNKuflONS11GuzVIJA4noRExY+aYCLuWDN2Hj +wIpSzfA9uk5P13sydtiIstrfQrI5bHXMT8vCOe2vugIm/dTcRGkn65OlUiQYRhI/ +0/oef9ulnpTmZa0sJ2LPGiUkbRNRsw1imIpzy0F3CdIBfVzlEri+wbIth3Ufaeuk +LelKitEXM+BeuCfAJGzBENzOL3RdSP6LwM6oDFxZxTu6nFJ+Kjx7CZeJECEGBZHj +fnZoZ5X9LSfNMYH4TZG0jkH2Sm7L0OQmPOqViZ5NB3bw +-----END CERTIFICATE----- diff --git a/spec/integration/gitlab_spec.rb b/spec/integration/gitlab_spec.rb index 3810c78b3..d17464d3b 100644 --- a/spec/integration/gitlab_spec.rb +++ b/spec/integration/gitlab_spec.rb @@ -77,4 +77,17 @@ RSpec.describe GitLab, type: :integration, required_envs: %w[GITLAB_ENDPOINT GIT end end end + + describe '#variables' do + describe 'Zammad ignores relative GitLab URLs #3830' do + let(:endpoint) { ENV['GITLAB_ENDPOINT'].sub('api/graphql', 'subfolder/api/graphql') } + let(:instance) { described_class.new(endpoint, ENV['GITLAB_APITOKEN']) } + let(:issue_url) { "https://#{URI.parse(ENV['GITLAB_ISSUE_LINK']).host}/subfolder/group/project/-/issues/1" } + let(:linked_issue) { GitLab::LinkedIssue.new(instance.client) } + + it 'does remove the subfolder from the fullpath to get the issue correctly' do + expect(linked_issue.send(:variables, issue_url)[:fullpath]).to eq('group/project') + end + end + end end diff --git a/spec/jobs/concerns/has_ticket_create_screen_impact_spec.rb b/spec/jobs/concerns/has_ticket_create_screen_impact_spec.rb deleted file mode 100644 index 63b84c3af..000000000 --- a/spec/jobs/concerns/has_ticket_create_screen_impact_spec.rb +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ - -require 'rails_helper' - -RSpec.describe HasTicketCreateScreenImpact, type: :job do - - context 'with groups' do - let!(:group) { create(:group) } - - it 'create should enqueue no job' do - collection_jobs = enqueued_jobs.select do |job| - job[:job] == TicketCreateScreenJob - end - expect(collection_jobs.count).to be(1) - end - - context 'updating attribute' do - before do - clear_jobs - end - - context 'name' do - it 'enqueues a job' do - expect { group.update!(name: 'new name') }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'active' do - it 'enqueues a job' do - expect { group.update!(active: false) }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'updated_at' do - it 'enqueues a job' do - expect { group.touch }.to have_enqueued_job(TicketCreateScreenJob) - end - end - end - - it 'delete should enqueue no job' do - clear_jobs - expect { group.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'with roles' do - let!(:role) { create(:role) } - - it 'create should enqueue no job' do - collection_jobs = enqueued_jobs.select do |job| - job[:job] == TicketCreateScreenJob - end - expect(collection_jobs.count).to be(1) - end - - context 'updating attribute' do - - before do - clear_jobs - end - - context 'name' do - it 'enqueues a job' do - expect { role.update!(name: 'new name') }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'active' do - it 'enqueues a job' do - expect { role.update!(active: false) }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'updated_at' do - it 'enqueues no job' do - expect { role.touch }.to have_enqueued_job(TicketCreateScreenJob) - end - end - end - - it 'delete should enqueue no job' do - clear_jobs - expect { role.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'with users' do - - let!(:customer) { create(:user, roles: Role.where(name: 'Customer')) } - let!(:agent) { create(:user, roles: Role.where(name: 'Agent')) } - let!(:admin) { create(:user, roles: Role.where(name: 'Admin')) } - - let(:customer_new) { create(:user, roles: Role.where(name: 'Customer')) } - let(:agent_new) { create(:user, roles: Role.where(name: 'Agent')) } - let(:admin_new) { create(:user, roles: Role.where(name: 'Admin')) } - - context 'creating' do - before do - clear_jobs - end - - it 'customer should enqueue no job' do - customer_new - collection_jobs = enqueued_jobs.select do |job| - job[:job] == TicketCreateScreenJob - end - expect(collection_jobs.count).to be(0) - end - - it 'agent should enqueue a job' do - agent_new - collection_jobs = enqueued_jobs.select do |job| - job[:job] == TicketCreateScreenJob - end - expect(collection_jobs.count).to be(1) - end - - it 'admin should enqueue no job' do - admin_new - collection_jobs = enqueued_jobs.select do |job| - job[:job] == TicketCreateScreenJob - end - expect(collection_jobs.count).to be(0) - end - end - - context 'updating attribute' do - before do - clear_jobs - end - - context 'firstname field for' do - it 'customer should enqueue no job' do - expect { customer.update!(firstname: 'new firstname') }.not_to have_enqueued_job(TicketCreateScreenJob) - end - - it 'agent should enqueue a job' do - expect { agent.update!(firstname: 'new firstname') }.to have_enqueued_job(TicketCreateScreenJob) - end - - it 'admin should enqueue no job' do - expect { admin.update!(firstname: 'new firstname') }.not_to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'active field for' do - it 'customer should enqueue no job' do - expect { customer.update!(active: false) }.not_to have_enqueued_job(TicketCreateScreenJob) - end - - it 'agent should enqueue a job' do - expect { agent.update!(active: false) }.to have_enqueued_job(TicketCreateScreenJob) - end - - it 'admin should enqueue no job' do - admin_new # Prevend "Minimum one user needs to have admin permissions." - clear_jobs - expect { admin.update!(active: false) }.not_to have_enqueued_job(TicketCreateScreenJob) - end - end - end - - context 'deleting' do - before do - clear_jobs - end - - it 'customer should enqueue a job' do - expect { customer.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - - it 'agent should enqueue a job' do - expect { agent.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - - it 'admin should enqueue a job' do - expect { admin.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - end - -end diff --git a/spec/lib/auth_spec.rb b/spec/lib/auth_spec.rb index 7bedbcefb..a95c26acd 100644 --- a/spec/lib/auth_spec.rb +++ b/spec/lib/auth_spec.rb @@ -168,6 +168,30 @@ RSpec.describe Auth do allow(Ldap::User).to receive(:new).with(any_args).and_return(ldap_user) end + shared_examples 'check empty password' do + before do + # Remove adapter from auth developer setting, to avoid execution for this test case, because of special empty + # password handling in adapter. + Setting.set('auth_developer', {}) + end + + context 'with empty password string' do + let(:password) { '' } + + it 'returns false' do + expect(instance.valid?).to be false + end + end + + context 'when password is nil' do + let(:password) { nil } + + it 'returns false' do + expect(instance.valid?).to be false + end + end + end + context 'with a ldap user without internal password' do let(:user) { create(:user, source: 'Ldap') } let(:password) { password_ldap } @@ -197,6 +221,8 @@ RSpec.describe Auth do expect { instance.valid? }.not_to change { user.reload.login_failed } end end + + include_examples 'check empty password' end context 'with a ldap user which also has a internal password' do @@ -238,6 +264,8 @@ RSpec.describe Auth do expect(instance.valid?).to be true end end + + include_examples 'check empty password' end end end diff --git a/spec/lib/core_ext/relation/as_batches_spec.rb b/spec/lib/core_ext/relation/as_batches_spec.rb new file mode 100644 index 000000000..5bd094c6b --- /dev/null +++ b/spec/lib/core_ext/relation/as_batches_spec.rb @@ -0,0 +1,41 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe 'AsBatches' do + def priorities_asc(size) + result = [] + Ticket::Priority.order(name: :asc).as_batches(size: size) do |prio| + result << prio + end + result + end + + def priorities_desc(size) + result = [] + Ticket::Priority.order(name: :desc).as_batches(size: size) do |prio| + result << prio + end + result + end + + context 'when batch is smaller then total result' do + it 'does return all priorities ascending' do + expect(priorities_asc(1)).to eq([ Ticket::Priority.find_by(name: '1 low'), Ticket::Priority.find_by(name: '2 normal'), Ticket::Priority.find_by(name: '3 high') ]) + end + + it 'does return all priorities decending' do + expect(priorities_desc(1)).to eq([ Ticket::Priority.find_by(name: '3 high'), Ticket::Priority.find_by(name: '2 normal'), Ticket::Priority.find_by(name: '1 low') ]) + end + end + + context 'when batch is equal to total result' do + it 'does return all priorities ascending' do + expect(priorities_asc(100)).to eq([ Ticket::Priority.find_by(name: '1 low'), Ticket::Priority.find_by(name: '2 normal'), Ticket::Priority.find_by(name: '3 high') ]) + end + + it 'does return all priorities decending' do + expect(priorities_desc(100)).to eq([ Ticket::Priority.find_by(name: '3 high'), Ticket::Priority.find_by(name: '2 normal'), Ticket::Priority.find_by(name: '1 low') ]) + end + end +end diff --git a/spec/lib/escalation_spec.rb b/spec/lib/escalation_spec.rb index 7d6a012ff..08d52ffeb 100644 --- a/spec/lib/escalation_spec.rb +++ b/spec/lib/escalation_spec.rb @@ -3,13 +3,18 @@ require 'rails_helper' RSpec.describe ::Escalation do - let(:instance) { described_class.new ticket, force: force } + let(:instance) { described_class.new ticket, force: force } let(:instance_with_history) { described_class.new ticket_with_history, force: force } + let(:instance_with_open) { described_class.new open_ticket_with_history, force: force } + let(:ticket) { create(:ticket) } let(:force) { false } - let(:sla) { nil } - let(:sla_247) { create(:sla, :condition_blank, first_response_time: 60, update_time: 60, solution_time: 75, calendar: create(:calendar, :'24/7')) } - let(:calendar) { nil } + let(:calendar) { create(:calendar, :'24/7') } + + let(:sla_247) { create(:sla, :condition_blank, solution_time: 75, calendar: calendar) } + let(:sla_247_response) { create(:sla, :condition_blank, first_response_time: 30, response_time: 45, solution_time: 75, calendar: calendar) } + let(:sla_247_update) { create(:sla, :condition_blank, first_response_time: 30, update_time: 60, solution_time: 75, calendar: calendar) } + let(:ticket_with_history) do freeze_time ticket = create(:ticket) @@ -252,7 +257,7 @@ RSpec.describe ::Escalation do # https://github.com/zammad/zammad/issues/3140 it 'customer contact sets #update_escalation_at' do - sla_247 + sla_247_response ticket create(:ticket_article, :inbound_email, ticket: ticket) @@ -261,7 +266,7 @@ RSpec.describe ::Escalation do context 'with ticket with sla and customer enquiry' do before do - sla_247 + sla_247_response ticket travel 10.minutes @@ -289,34 +294,120 @@ RSpec.describe ::Escalation do let(:force) { true } # initial calculation it 'returns attribute' do - sla_247 + sla_247_response allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) result = instance_with_history.send(:escalation_first_response) - expect(result).to include first_response_escalation_at: 60.minutes.ago + expect(result).to include first_response_escalation_at: 90.minutes.ago end it 'returns nil when no sla#first_response_time' do - sla_247.update! first_response_time: nil + sla_247_response.update! first_response_time: nil allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) result = instance_with_history.send(:escalation_first_response) expect(result).to include(first_response_escalation_at: nil) end end - describe '#escalation_update' do - it 'returns attribute' do + describe '#escalation_update_reset' do + it 'resets to nil when no sla#response_time and sla#update_time' do sla_247 - ticket_with_history.last_contact_customer_at = 2.hours.ago allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) - result = instance_with_history.send(:escalation_update) - expect(result).to include update_escalation_at: 60.minutes.ago + result = instance_with_history.send(:escalation_update_reset) + expect(result).to include(update_escalation_at: nil) + end + + it 'returns nil when no sla#response_time' do + sla_247_update + allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) + result = instance_with_history.send(:escalation_update_reset) + expect(result).to be_nil end it 'returns nil when no sla#update_time' do - sla_247.update! update_time: nil + sla_247_response allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) - result = instance_with_history.send(:escalation_update) - expect(result).to include(update_escalation_at: nil) + result = instance_with_history.send(:escalation_update_reset) + expect(result).to be_nil + end + end + + describe '#escalation_response' do + it 'returns attribute' do + sla_247_response + ticket_with_history.last_contact_customer_at = 2.hours.ago + allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) + result = instance_with_history.send(:escalation_response) + expect(result).to include update_escalation_at: 75.minutes.ago + end + + it 'returns nil when no sla#response_time' do + sla_247 + allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) + result = instance_with_history.send(:escalation_response) + expect(result).to be_nil + end + + it 'response time is calculated when waiting for the first response with update-only SLA' do + sla_247_response.update! first_response_time: nil + ticket_with_history.last_contact_customer_at = 2.hours.ago + allow(instance_with_history).to receive(:escalation_disabled?).and_return(false) + result = instance_with_history.send(:escalation_response) + expect(result).to include update_escalation_at: 75.minutes.ago + end + end + + describe '#escalation_update' do + context 'when has open ticket with history' do + before do + sla_247_update + open_ticket_with_history + allow(instance_with_open).to receive(:escalation_disabled?).and_return(false) + end + + it 'update time is calculated before first agent response' do + result = instance_with_open.send(:escalation_update) + expect(result).to include update_escalation_at: 50.minutes.from_now + end + + it 'update time is calculated after agent response' do + create(:ticket_article, :outbound_email, ticket: open_ticket_with_history) + result = instance_with_open.send(:escalation_update) + expect(result).to include update_escalation_at: 60.minutes.from_now + end + + context 'when agent responds' do + before do + create(:ticket_article, :outbound_email, ticket: open_ticket_with_history) + travel 30.minutes + end + + it 'update time is calculated after 2nd customer enquiry' do + create(:ticket_article, :inbound_email, ticket: open_ticket_with_history) + result = instance_with_open.send(:escalation_update) + expect(result).to include update_escalation_at: 30.minutes.from_now + end + + it 'update time is calculated after 2nd agent response interrupted by customer' do + create(:ticket_article, :inbound_email, ticket: open_ticket_with_history) + travel 30.minutes + create(:ticket_article, :outbound_email, ticket: open_ticket_with_history) + result = instance_with_open.send(:escalation_update) + expect(result).to include update_escalation_at: 60.minutes.from_now + end + + it 'update time is calculated after 2nd agent response in a row' do + create(:ticket_article, :outbound_email, ticket: open_ticket_with_history) + result = instance_with_open.send(:escalation_update) + expect(result).to include update_escalation_at: 60.minutes.from_now + end + end + end + + it 'returns nil when no sla#update_time' do + sla_247 + allow(instance_with_open).to receive(:escalation_disabled?).and_return(false) + result = instance_with_open.send(:escalation_update) + expect(result).to be_nil end end @@ -387,16 +478,16 @@ RSpec.describe ::Escalation do describe '#statistics_first_response' do it 'calculates statistics' do - sla_247 + sla_247_response ticket_with_history.first_response_at = 45.minutes.ago instance_with_history.force! result = instance_with_history.send(:statistics_first_response) - expect(result).to include(first_response_in_min: 75, first_response_diff_in_min: -15) + expect(result).to include(first_response_in_min: 75, first_response_diff_in_min: -45) end it 'does not touch statistics when sla time is nil' do - sla_247.update! first_response_time: nil + sla_247_response.update! first_response_time: nil ticket_with_history.first_response_at = 45.minutes.ago instance_with_history.force! @@ -405,9 +496,9 @@ RSpec.describe ::Escalation do end end - describe '#statistics_update' do + describe '#statistics_response' do before do - sla_247 + sla_247_response freeze_time end @@ -415,22 +506,22 @@ RSpec.describe ::Escalation do ticket_with_history.last_contact_customer_at = 61.minutes.ago ticket_with_history.last_contact_agent_at = 60.minutes.ago - result = instance_with_history.send(:statistics_update) - expect(result).to include(update_in_min: 1, update_diff_in_min: 59) + result = instance_with_history.send(:statistics_response) + expect(result).to include(update_in_min: 1, update_diff_in_min: 44) end it 'does not calculate statistics when customer respose is last' do ticket_with_history.last_contact_customer_at = 59.minutes.ago ticket_with_history.last_contact_agent_at = 60.minutes.ago - result = instance_with_history.send(:statistics_update) + result = instance_with_history.send(:statistics_response) expect(result).to be_nil end it 'does not calculate statistics when only customer enquiry present' do create(:ticket_article, :inbound_email, ticket: ticket) - result = instance.send(:statistics_update) + result = instance.send(:statistics_response) expect(result).to be_nil end @@ -440,7 +531,7 @@ RSpec.describe ::Escalation do create(:ticket_article, :outbound_email, ticket: ticket) instance.force! - expect(instance.send(:statistics_update)).to include(update_in_min: 10, update_diff_in_min: 50) + expect(instance.send(:statistics_response)).to include(update_in_min: 10, update_diff_in_min: 35) end context 'with multiple exchanges and later one being quicker' do @@ -455,7 +546,95 @@ RSpec.describe ::Escalation do end it 'keeps statistics of longest exchange' do - expect(ticket.reload).to have_attributes(update_in_min: 10, update_diff_in_min: 50) + expect(ticket.reload).to have_attributes(update_in_min: 10, update_diff_in_min: 35) + end + end + + it 'does not touch statistics when sla time is nil' do + sla_247.update! update_time: nil + ticket_with_history.last_contact_customer_at = 60.minutes.ago + instance_with_history.force! + + result = instance_with_history.send(:statistics_update) + expect(result).to be_nil + end + + it 'does not touch statistics when last update is nil' do + ticket_with_history.assign_attributes last_contact_agent_at: nil, last_contact_customer_at: nil + instance_with_history.force! + + result = instance_with_history.send(:statistics_update) + expect(result).to be_nil + end + end + + describe '#statistics_update' do + before do + sla_247_update + freeze_time + end + + it 'does not calculate statistics when only customer enquiry present' do + create(:ticket_article, :inbound_email, ticket: ticket) + + result = instance.send(:statistics_update) + expect(result).to be_nil + end + + context 'when agent responds after 20 minutes' do + before do + ticket + travel 20.minutes + create(:ticket_article, :outbound_email, ticket: ticket) + end + + it 'does not touch statistics when customer response is most recent' do + travel 30.minutes + create(:ticket_article, :inbound_email, ticket: ticket) + + result = instance.send(:statistics_update) + expect(result).to include(update_diff_in_min: 40, update_in_min: 20) + end + + it 'calculates statistics when only agent update present' do + result = instance.send(:statistics_update) + expect(result).to include(update_diff_in_min: 40, update_in_min: 20) + end + + it 'calculates statistics when multiple agent updates present' do + travel 30.minutes + create(:ticket_article, :outbound_email, ticket: ticket) + + result = instance.send(:statistics_update) + expect(result).to include(update_diff_in_min: 30, update_in_min: 30) + end + + context 'when customer responds' do + before do + travel 10.minutes + create(:ticket_article, :inbound_email, ticket: ticket) + end + + it 'calculates statistics when multiple agent updates intercepted by customer' do + travel 35.minutes + create(:ticket_article, :outbound_email, ticket: ticket) + + result = instance.send(:statistics_update) + expect(result).to include(update_diff_in_min: 15, update_in_min: 45) + end + end + end + + context 'with multiple exchanges and later one being quicker' do + before do + travel 10.minutes + create(:ticket_article, :outbound_email, ticket: ticket) + travel 5.minutes + create(:ticket_article, :outbound_email, ticket: ticket) + end + + it 'keeps statistics of longest exchange' do + expect(ticket.reload).to have_attributes(update_in_min: 5, update_diff_in_min: 55) end end @@ -512,7 +691,7 @@ RSpec.describe ::Escalation do it 'switching state pushes escalation date' do sla_247 open_ticket_with_history.reload - expect(open_ticket_with_history.update_escalation_at).to eq open_ticket_with_history.created_at + 70.minutes + expect(open_ticket_with_history.escalation_at).to eq open_ticket_with_history.created_at + 85.minutes end def without_update_escalation_information_callback(&block) diff --git a/spec/lib/report/ticket_first_solution_spec.rb b/spec/lib/report/ticket_first_solution_spec.rb new file mode 100644 index 000000000..bd742f8da --- /dev/null +++ b/spec/lib/report/ticket_first_solution_spec.rb @@ -0,0 +1,192 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +# rubocop:disable RSpec/ExampleLength + +require 'rails_helper' +require 'lib/report_examples' + +RSpec.describe Report::TicketFirstSolution, searchindex: true do + include_examples 'with report examples' + + describe '.aggs' do + it 'gets monthly aggregated results' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: {}, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0] + end + + it 'gets monthly aggregated results with high priority' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket.priority_id' => { + 'operator' => 'is', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + end + + it 'gets monthly aggregated results not in merged state' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket_state.name' => { + 'operator' => 'is not', + 'value' => 'merged', + } + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0] + end + + it 'gets monthly aggregated results with not high priority' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket.priority_id' => { + 'operator' => 'is not', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0] + end + + it 'gets weekly aggregated results' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-10-26T00:00:00Z'), + range_end: Time.zone.parse('2015-10-31T23:59:59Z'), + interval: 'week', + selector: {}, + ) + + expect(result).to eq [0, 0, 1, 0, 0, 1, 1] + end + + it 'gets daily aggregated results' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-10-01T00:00:00Z'), + range_end: Time.zone.parse('2015-11-01T23:59:59Z'), + interval: 'day', + selector: {}, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1] + end + + it 'gets hourly aggregated results' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-10-28T00:00:00Z'), + range_end: Time.zone.parse('2015-10-28T23:59:59Z'), + interval: 'hour', + selector: {}, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + end + end + + describe '.items' do + it 'gets items in year range' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: {}, + ) + expect(result).to match_tickets ticket_5, ticket_6, ticket_7 + end + + it 'gets items in year range with high priority' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket.priority_id' => { + 'operator' => 'is', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + } + ) + + expect(result).to match_tickets ticket_5 + end + + it 'gets items in year range not in merged state' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket_state.name' => { + 'operator' => 'is not', + 'value' => 'merged', + } + } + ) + + expect(result).to match_tickets ticket_5, ticket_6, ticket_7 + end + + it 'gets items in year range with not high priority' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket.priority_id' => { + 'operator' => 'is not', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + } + ) + + expect(result).to match_tickets ticket_6, ticket_7 + end + + it 'gets items in week range' do + result = described_class.items( + range_start: Time.zone.parse('2015-10-26T00:00:00Z'), + range_end: Time.zone.parse('2015-11-01T23:59:59Z'), + selector: {} + ) + + expect(result).to match_tickets ticket_5, ticket_6, ticket_7 + end + + it 'gets items in day range' do + result = described_class.items( + range_start: Time.zone.parse('2015-10-01T00:00:00Z'), + range_end: Time.zone.parse('2015-10-31T23:59:59Z'), + selector: {} + ) + + expect(result).to match_tickets ticket_5, ticket_6 + end + + it 'gets items in hour range' do + result = described_class.items( + range_start: Time.zone.parse('2015-10-28T00:00:00Z'), + range_end: Time.zone.parse('2015-10-28T23:59:59Z'), + interval: 'hour', + selector: {}, + ) + + expect(result).to match_tickets ticket_5 + end + end +end +# rubocop:enable RSpec/ExampleLength diff --git a/spec/lib/report/ticket_generic_time_spec.rb b/spec/lib/report/ticket_generic_time_spec.rb index 20f2fc512..cbf8fa24f 100644 --- a/spec/lib/report/ticket_generic_time_spec.rb +++ b/spec/lib/report/ticket_generic_time_spec.rb @@ -1,33 +1,333 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ require 'rails_helper' +require 'lib/report_examples' -RSpec.describe Report::TicketGenericTime do +RSpec.describe Report::TicketGenericTime, searchindex: true do + include_examples 'with report examples' -=begin + describe '.aggs' do + it 'gets monthly aggregated results by created_at' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', # year, quarter, month, week, day, hour, minute, second + selector: {}, # ticket selector to get only a collection of tickets + params: { field: 'created_at' }, + ) - result = Report::TicketGenericTime.items( - range_start: '2015-01-01T00:00:00Z', - range_end: '2015-12-31T23:59:59Z', - selector: selector, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 1, 0] + end -returns + it 'gets monthly aggregated results by created_at not merged' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', # year, quarter, month, week, day, hour, minute, second + selector: { + 'state' => { + 'operator' => 'is not', + 'value' => 'merged' + } + }, + params: { field: 'created_at' }, + ) - { - count: 123, - ticket_ids: [4,5,1,5,0,51,5,56,7,4], - assets: assets, - } + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 1, 0] + end + end -=end + describe '.items' do + it 'gets items in year range by created_at' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: {}, # ticket selector to get only a collection of tickets + params: { field: 'created_at' }, + ) - describe 'items' do + expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1 + end + + it 'gets items in year range by created_at not merged' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'state' => { + 'operator' => 'is not', + 'value' => 'merged' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1 + end + + it 'gets items in year range by created_at before oct 31st' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'created_at' => { + 'operator' => 'before (absolute)', + 'value' => '2015-10-31T00:00:00Z' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_5, ticket_4, ticket_3, ticket_2, ticket_1 + end + + it 'gets items in year range by created_at after oct 31st' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'created_at' => { + 'operator' => 'after (absolute)', + 'value' => '2015-10-31T00:00:00Z' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_7, ticket_6 + end + + it 'gets items in 1 day from now' do + result = described_class.items( + range_start: 1.year.ago.beginning_of_year, + range_end: 1.year.from_now.at_end_of_year, + selector: { + 'created_at' => { + 'operator' => 'after (relative)', + 'range' => 'day', + 'value' => '1' + } + }, # ticket selector to get only a collection of tickets + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_after_72h + end + + it 'gets items in 1 month from now' do + result = described_class.items( + range_start: 1.year.ago.beginning_of_year, + range_end: 1.year.from_now.at_end_of_year, + selector: { + 'created_at' => { + 'operator' => 'after (relative)', + 'range' => 'month', + 'value' => '1' + } + }, # ticket selector to get only a collection of tickets + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets [] + end + + it 'gets items in 1 month ago' do + result = described_class.items( + range_start: 1.year.ago.beginning_of_year, + range_end: 1.year.from_now.at_end_of_year, + selector: { + 'created_at' => { + 'operator' => 'before (relative)', + 'range' => 'month', + 'value' => '1' + } + }, # ticket selector to get only a collection of tickets + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_before_40d + end + + it 'gets items in 5 months ago' do + result = described_class.items( + range_start: 1.year.ago.beginning_of_year, + range_end: 1.year.from_now.at_end_of_year, + selector: { + 'created_at' => { + 'operator' => 'before (relative)', + 'range' => 'month', + 'value' => '5' + } + }, # ticket selector to get only a collection of tickets + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets [] + end + + it 'gets items with aaa+bbb' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains all', + 'value' => 'aaa, bbb' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_1 + end + + it 'gets items with not aaa+bbb' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains all not', + 'value' => 'aaa, bbb' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2 + end + + it 'gets items with aaa' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains all', + 'value' => 'aaa' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_2, ticket_1 + end + + it 'gets items with not aaa' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains all not', + 'value' => 'aaa' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3 + end + + it 'gets items with one not aaa' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains one not', + 'value' => 'aaa' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3 + end + + it 'gets items with one not aaa+bbb' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains one not', + 'value' => 'aaa, bbb' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_7, ticket_6, ticket_4, ticket_3 + end + + it 'gets items with one aaa' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains one', + 'value' => 'aaa' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_2, ticket_1 + end + + it 'gets items with one aaa+bbb' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'tags' => { + 'operator' => 'contains one', + 'value' => 'aaa, bbb' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_5, ticket_2, ticket_1 + end + + it 'gets items with test' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'title' => { + 'operator' => 'contains', + 'value' => 'Test' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_7, ticket_6, ticket_5, ticket_4, ticket_3, ticket_2, ticket_1 + end + + it 'gets items with not test' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'title' => { + 'operator' => 'contains not', + 'value' => 'Test' + } + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets [] + end # Regression test for issue #2246 - Records in Reporting not updated when single ActiveRecord can not be found - it 'correctly handles missing tickets' do - class_double('SearchIndexBackend', selectors: { ticket_ids: [-1] }).as_stubbed_const + it 'correctly handles missing tickets', searchindex: false do + class_double('SearchIndexBackend', selectors: { ticket_ids: [-1] }, drop_index: nil, drop_pipeline: nil).as_stubbed_const expect do described_class.items( @@ -39,4 +339,78 @@ returns end.not_to raise_error end end + + context 'when additional attribute exists', db_strategy: :reset do + before do + ObjectManager::Attribute.add( + object: 'Ticket', + name: 'test_category', + display: 'Test 1', + data_type: 'tree_select', + data_option: { + maxlength: 200, + null: false, + default: '', + options: [ + { 'name' => 'aa', 'value' => 'aa', 'children' => [{ 'name' => 'aa', 'value' => 'aa::aa' }, { 'name' => 'bb', 'value' => 'aa::bb' }, { 'name' => 'cc', 'value' => 'aa::cc' }] }, + { 'name' => 'bb', 'value' => 'bb', 'children' => [{ 'name' => 'aa', 'value' => 'bb::aa' }, { 'name' => 'bb', 'value' => 'bb::bb' }, { 'name' => 'cc', 'value' => 'bb::cc' }] }, + { 'name' => 'cc', 'value' => 'cc', 'children' => [{ 'name' => 'aa', 'value' => 'cc::aa' }, { 'name' => 'bb', 'value' => 'cc::bb' }, { 'name' => 'cc', 'value' => 'cc::cc' }] }, + ] + }, + active: true, + screens: {}, + position: 20, + created_by_id: 1, + updated_by_id: 1, + editable: false, + to_migrate: false, + ) + ObjectManager::Attribute.migration_execute + + ticket_with_category + + rebuild_searchindex + end + + let(:ticket_with_category) do + travel_to DateTime.new 2015, 10, 28, 9, 30 + ticket = create(:ticket, + group: group_2, + customer: customer, + test_category: 'cc::bb', + state_name: 'new', + priority_name: '2 normal') + + ticket.tag_add('aaa', 1) + ticket.tag_add('bbb', 1) + create(:ticket_article, + :inbound_email, + ticket: ticket) + + travel 5.hours + + ticket.update! group: group_1 + + travel_back + ticket + end + + describe '.items' do + it 'gets items with test_category cc:bb' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'test_category' => { + 'operator' => 'is', + 'value' => 'cc::bb' + }, + }, + params: { field: 'created_at' }, + ) + + expect(result).to match_tickets ticket_with_category + end + end + end end diff --git a/spec/lib/report/ticket_moved_spec.rb b/spec/lib/report/ticket_moved_spec.rb new file mode 100644 index 000000000..9efb30424 --- /dev/null +++ b/spec/lib/report/ticket_moved_spec.rb @@ -0,0 +1,148 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +# +# rubocop:disable RSpec/ExampleLength + +require 'rails_helper' +require 'lib/report_examples' + +RSpec.describe Report::TicketMoved, searchindex: true do + include_examples 'with report examples' + + describe '.aggs' do + it 'gets monthly aggregated results not in merged state' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket_state.name' => { + 'operator' => 'is not', + 'value' => 'merged', + } + }, + params: { + type: 'in', + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + end + + it 'gets monthly aggregated results in users group' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket.group_id' => { + 'operator' => 'is', + 'value' => [Group.lookup(name: 'Users').id], + } + }, + params: { + type: 'in', + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + end + + it 'gets monthly aggregated results not in merged state and outgoing' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket_state.name' => { + 'operator' => 'is not', + 'value' => 'merged', + } + }, + params: { + type: 'out', + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + end + + it 'gets monthly aggregated results in users group and outgoing' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket.group_id' => { + 'operator' => 'is', + 'value' => [Group.lookup(name: 'Users').id], + } + }, + params: { + type: 'out', + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + end + + end + + describe '.items' do + it 'gets items in year range in users group' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket.group_id' => { + 'operator' => 'is', + 'value' => [Group.lookup(name: 'Users').id], + } + }, + params: { + type: 'in', + }, + ) + + expect(result).to match_tickets ticket_1 + end + + it 'gets items in year range not merged and outgoing' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket_state.name' => { + 'operator' => 'is not', + 'value' => 'merged', + } + }, # ticket selector to get only a collection of tickets + params: { + type: 'out', + }, + ) + + expect(result).to match_tickets [] + end + + it 'gets items in year range in users group and outgoing' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket.group_id' => { + 'operator' => 'is', + 'value' => [Group.lookup(name: 'Users').id], + } + }, + params: { + type: 'out', + }, + ) + + expect(result).to match_tickets ticket_2 + end + + end +end +# rubocop:enable RSpec/ExampleLength diff --git a/spec/lib/report/ticket_reopened_spec.rb b/spec/lib/report/ticket_reopened_spec.rb new file mode 100644 index 000000000..054fc9663 --- /dev/null +++ b/spec/lib/report/ticket_reopened_spec.rb @@ -0,0 +1,129 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +# rubocop:disable RSpec/ExampleLength + +require 'rails_helper' +require 'lib/report_examples' + +RSpec.describe Report::TicketReopened, searchindex: true do + include_examples 'with report examples' + + describe '.aggs' do + it 'gets monthly aggregated results' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: {}, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + end + + it 'gets monthly aggregated results with high priority' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket.priority_id' => { + 'operator' => 'is', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + } + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + end + + it 'gets monthly aggregated results with not high priority' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket.priority_id' => { + 'operator' => 'is not', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + end + + it 'gets monthly aggregated results with not merged' do + result = described_class.aggs( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + interval: 'month', + selector: { + 'ticket_state.name' => { + 'operator' => 'is not', + 'value' => 'merged', + } + }, + ) + + expect(result).to eq [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + end + end + + describe '.items' do + it 'gets items in year range' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: {}, + ) + + expect(result).to match_tickets ticket_5 + end + + it 'gets items in year range with high priority' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket.priority_id' => { + 'operator' => 'is', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + }, + ) + + expect(result).to match_tickets ticket_5 + end + + it 'gets items in year range with not high priority' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket.priority_id' => { + 'operator' => 'is not', + 'value' => [Ticket::Priority.lookup(name: '3 high').id], + } + }, + ) + + expect(result).to match_tickets [] + end + + it 'gets items in year range with not merged' do + result = described_class.items( + range_start: Time.zone.parse('2015-01-01T00:00:00Z'), + range_end: Time.zone.parse('2015-12-31T23:59:59Z'), + selector: { + 'ticket_state.name' => { + 'operator' => 'is not', + 'value' => 'merged', + } + }, + ) + + expect(result).to match_tickets ticket_5 + end + end +end +# rubocop:enable RSpec/ExampleLength diff --git a/spec/lib/report_examples.rb b/spec/lib/report_examples.rb new file mode 100644 index 000000000..e770617b4 --- /dev/null +++ b/spec/lib/report_examples.rb @@ -0,0 +1,245 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +RSpec.shared_context 'with report examples' do + before do |example| + next if !example.metadata[:searchindex] + + configure_elasticsearch(required: true, rebuild: true) do + ticket_1 + + ticket_2 + + ticket_3 + + ticket_4 + + ticket_5 + + ticket_6 + + ticket_7 + + ticket_8 + + ticket_9 + + ticket_after_72h + + ticket_before_40d + end + end + + let(:group_1) { Group.lookup(name: 'Users') } + let(:group_2) { create(:group) } + let(:customer) { User.lookup(email: 'nicole.braun@zammad.org') } + + let(:ticket_1) do + travel_to DateTime.new 2015, 10, 28, 9, 30 + ticket = create(:ticket, + group: group_2, + customer: customer, + state_name: 'new', + priority_name: '2 normal') + + ticket.tag_add('aaa', 1) + ticket.tag_add('bbb', 1) + create(:ticket_article, + :inbound_email, + ticket: ticket) + + travel 5.hours + + ticket.update! group: group_1 + + travel_back + ticket + end + + let(:ticket_2) do + travel_to DateTime.new 2015, 10, 28, 9, 30, 1 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'new', + priority_name: '2 normal') + + ticket.tag_add('aaa', 1) + create(:ticket_article, + :inbound_email, + ticket: ticket) + travel 5.hours - 1.second + + ticket.update! group: group_2 + + travel_back + ticket + end + + let(:ticket_3) do + travel_to DateTime.new 2015, 10, 28, 10, 30 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'open', + priority_name: '3 high') + create(:ticket_article, + :inbound_email, + ticket: ticket) + + travel_back + ticket + end + + let(:ticket_4) do + travel_to DateTime.new 2015, 10, 28, 10, 30, 1 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'closed', + priority_name: '2 normal', + close_at: (1.hour - 1.second).from_now) + create(:ticket_article, + :inbound_email, + ticket: ticket) + travel_back + ticket + end + + let(:ticket_5) do + travel_to DateTime.new 2015, 10, 28, 11, 30 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'closed', + priority_name: '3 high', + close_at: 10.minutes.from_now) + + ticket.tag_add('bbb', 1) + create(:ticket_article, + :outbound_email, + ticket: ticket) + + ticket.update! state: Ticket::State.lookup(name: 'open') + + travel 3.hours + + travel_back + ticket + end + + let(:ticket_6) do + travel_to DateTime.new 2015, 10, 31, 12, 30 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'closed', + priority_name: '2 normal', + close_at: 5.minutes.from_now) + create(:ticket_article, + :outbound_email, + ticket: ticket) + + travel_back + ticket + end + + let(:ticket_7) do + travel_to DateTime.new 2015, 11, 1, 12, 30 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'closed', + priority_name: '2 normal', + close_at: Time.zone.now) + create(:ticket_article, + :inbound_email, + ticket: ticket) + travel_back + ticket + end + + let(:ticket_8) do + travel_to DateTime.new 2015, 11, 2, 12, 30 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'merged', + priority_name: '2 normal', + close_at: Time.zone.now) + + create(:ticket_article, + :inbound_email, + ticket: ticket) + + travel_back + ticket + end + + let(:ticket_9) do + travel_to DateTime.new 2037, 11, 2, 12, 30 + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'merged', + priority_name: '2 normal', + close_at: Time.zone.now) + create(:ticket_article, + :inbound_email, + ticket: ticket) + + travel_back + + ticket + end + + let(:ticket_after_72h) do + travel 72.hours do + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'closed', + priority_name: '2 normal', + close_at: 5.minutes.from_now) + create(:ticket_article, + :outbound_email, + ticket: ticket) + + ticket + end + end + + let(:ticket_before_40d) do + travel(-40.days) do + ticket = create(:ticket, + group: group_1, + customer: customer, + state_name: 'closed', + priority_name: '2 normal', + close_at: 5.minutes.from_now) + create(:ticket_article, + :outbound_email, + ticket: ticket) + + ticket + end + end + + matcher :match_tickets do + match do + if expected_tickets.blank? + actual_ticket_ids.blank? + else + # GenericTime returns string ids :o + actual_ticket_ids.map(&:to_i) == expected_tickets.map(&:id) + end + end + + def expected_tickets + Array(expected) + end + + def actual_ticket_ids + actual[:ticket_ids] + end + end +end diff --git a/spec/lib/search_index_backend_spec.rb b/spec/lib/search_index_backend_spec.rb index 5f3f46c03..c7f488c5b 100644 --- a/spec/lib/search_index_backend_spec.rb +++ b/spec/lib/search_index_backend_spec.rb @@ -2,11 +2,12 @@ require 'rails_helper' -RSpec.describe SearchIndexBackend, searchindex: true do +RSpec.describe SearchIndexBackend do - before do - configure_elasticsearch - rebuild_searchindex + before do |example| + next if !example.metadata[:searchindex] + + configure_elasticsearch(required: true, rebuild: true) end describe '.build_query' do @@ -19,7 +20,7 @@ RSpec.describe SearchIndexBackend, searchindex: true do end end - describe '.search' do + describe '.search', searchindex: true do context 'query finds results' do @@ -200,7 +201,8 @@ RSpec.describe SearchIndexBackend, searchindex: true do end end - describe '.remove' do + describe '.remove', searchindex: true do + context 'record gets deleted' do let(:record_type) { 'Ticket'.freeze } @@ -239,7 +241,7 @@ RSpec.describe SearchIndexBackend, searchindex: true do end end - describe '.selectors' do + describe '.selectors', searchindex: true do let(:group1) { create :group } let(:organization1) { create :organization, note: 'hihi' } @@ -845,4 +847,145 @@ RSpec.describe SearchIndexBackend, searchindex: true do end end end + + describe '.verify_date_range' do + let(:range_1) { { range: { created_at: { from: '2020-01-01T00:00:00.000Z', to: '2021-12-31T23:59:59Z' } } } } + let(:range_2) { { range: { created_at: { from: '2020-03-01T00:00:00.000Z', to: '2020-03-31T23:59:59Z' } } } } + let(:range_3) { { range: { created_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } } + let(:range_4) { { range: { updated_at: { from: '2018-03-01T00:00:00.000Z', to: '2018-03-31T23:59:59Z' } } } } + + def build_payload(*ranges) + { + query: { + bool: { + must: ranges + } + } + } + end + + it 'verifies single range' do + result = described_class.verify_date_range 'url', build_payload(range_1) + + expect(result).to be_truthy + end + + it 'verifies multiple intersecting ranges' do + result = described_class.verify_date_range 'url', build_payload(range_1, range_2) + + expect(result).to be_truthy + end + + it 'verifies non-intersecting ranges on different keys' do + result = described_class.verify_date_range 'url', build_payload(range_1, range_4) + + expect(result).to be_truthy + end + + it 'verifies payload without any ranges' do + result = described_class.verify_date_range 'url', build_payload + + expect(result).to be_truthy + end + + it 'verifies payload without payload' do + result = described_class.verify_date_range 'url', {} + + expect(result).to be_truthy + end + + it 'raises an error with multiple non-intersecting range' do + expect { described_class.verify_date_range 'url', build_payload(range_1, range_3) } + .to raise_error(%r{Conflicting date ranges}) + end + + context 'with a stubbed range' do + before do + allow(described_class).to receive(:convert_es_date_range).and_return(mock_range) + end + + let(:mock_range) { instance_double('Range', overlaps?: true) } + + it 'checks overlap once for 2 ranges' do + described_class.verify_date_range 'url', build_payload(range_1, range_2) + expect(mock_range).to have_received(:overlaps?).exactly(1).times + end + + it 'checks overlap 3 times for 3 ranges' do + described_class.verify_date_range 'url', build_payload(range_1, range_2, range_3) + expect(mock_range).to have_received(:overlaps?).exactly(3).times + end + end + end + + describe '.verify_single_key_range' do + let(:range_1) { DateTime.new(2020, 1, 1)..DateTime.new(2021, 12, 31) } + let(:range_2) { DateTime.new(2020, 3, 1)..DateTime.new(2020, 3, 31) } + let(:range_3) { DateTime.new(2018, 3, 1)..DateTime.new(2018, 3, 31) } + + it 'returns true with a single range' do + result = described_class.verify_single_key_range [range_1] + expect(result).to be_truthy + end + + it 'returns true with overlapping ranges' do + result = described_class.verify_single_key_range [range_1, range_2] + expect(result).to be_truthy + end + + it 'returns false with non-overlapping ranges' do + result = described_class.verify_single_key_range [range_1, range_3] + expect(result).to be_falsey + end + end + + describe '.convert_es_date_range' do + let(:from) { DateTime.new 2018, 1, 1, 17 } + let(:from_placeholder) { DateTime.new(-9999, 1, 1) } + let(:to) { DateTime.new 2020, 10, 1, 23 } + let(:to_placeholder) { DateTime.new 9999, 1, 1 } + + it 'converts range' do + result = described_class.convert_es_date_range( + { + range: { + created_at: { + from: '2018-01-01T17:00:00.000Z', + to: '2020-10-01T23:00:00Z' + } + } + } + ) + + expect(result).to eq from..to + end + + it 'converts less than' do + result = described_class.convert_es_date_range( + { + range: { + created_at: { + lt: '2020-10-01T23:00:00Z' + } + } + } + ) + + expect(result).to eq from_placeholder..to + end + + it 'converts greater than' do + result = described_class.convert_es_date_range( + { + range: { + created_at: { + gt: '2018-01-01T17:00:00.000Z', + } + } + } + ) + + expect(result).to eq from..to_placeholder + end + end end diff --git a/spec/lib/sequencer/unit/freshdesk/permission_present_spec.rb b/spec/lib/sequencer/unit/freshdesk/permission_present_spec.rb new file mode 100644 index 000000000..1bd46e88e --- /dev/null +++ b/spec/lib/sequencer/unit/freshdesk/permission_present_spec.rb @@ -0,0 +1,31 @@ +# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe Sequencer::Unit::Freshdesk::PermissionPresent, sequencer: :unit do + + context 'when checking the permission to Freshdesk' do + + let(:params) do + { + dry_run: false, + import_job: instance_double(ImportJob), + field_map: {}, + id_map: {}, + } + end + + let(:response_ok) { Net::HTTPOK.new(1.0, '200', 'OK') } + let(:response_forbidden) { Net::HTTPForbidden.new(1.0, '403', 'Forbidden') } + + it 'check for correct permission' do + allow(described_class).to receive(:perform_request).with(any_args).and_return(response_ok) + expect(process(params)).to eq({ permission_present: true }) + end + + it 'check for forbidden permission' do + allow(described_class).to receive(:perform_request).with(any_args).and_return(response_forbidden) + expect(process(params)).to eq({ permission_present: false }) + end + end +end diff --git a/spec/models/calendar_spec.rb b/spec/models/calendar_spec.rb index f2f66cab8..77aa7a91f 100644 --- a/spec/models/calendar_spec.rb +++ b/spec/models/calendar_spec.rb @@ -43,6 +43,19 @@ RSpec.describe Calendar, type: :model do expect { described_class.first.destroy } .to change { calendar.reload.default }.to(true) end + + context 'when sla has destroyed calendar set' do + let(:sla) { create(:sla, calendar: described_class.first) } + + before do + sla + end + + it 'sets the new default calendar to the sla' do + expect { described_class.first.destroy } + .to change { sla.reload.calendar }.to(calendar) + end + end end end @@ -303,7 +316,7 @@ RSpec.describe Calendar, type: :model do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: nil) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: nil) } before do queue_adapter.perform_enqueued_jobs = true @@ -400,7 +413,7 @@ RSpec.describe Calendar, type: :model do calendar: calendar, condition: {}, first_response_time: 120, - update_time: 180, + response_time: 180, solution_time: 240) end diff --git a/spec/models/concerns/has_ticket_create_screen_impact_examples.rb b/spec/models/concerns/has_ticket_create_screen_impact_examples.rb deleted file mode 100644 index 03e778184..000000000 --- a/spec/models/concerns/has_ticket_create_screen_impact_examples.rb +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ - -RSpec.shared_examples 'HasTicketCreateScreenImpact' do |create_screen_factory:| - - describe '#push_ticket_create_screen', performs_jobs: true do - subject { create(create_screen_factory) } - - context 'creating a record' do - it 'enqueues a TicketCreateScreenJob job' do - expect { subject }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'record exists' do - - before do - subject - clear_jobs - end - - context 'attribute updated' do - - context 'name' do - it 'enqueues a TicketCreateScreenJob job' do - expect do - subject.name = 'New name' - subject.save! - end.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'updated_at' do - it 'enqueues a TicketCreateScreenJob job' do - expect { subject.touch }.to have_enqueued_job(TicketCreateScreenJob) - end - end - end - - context 'record is deleted' do - it 'enqueues a TicketCreateScreenJob job' do - expect { subject.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - end - end - end -end diff --git a/spec/models/core_workflow_spec.rb b/spec/models/core_workflow_spec.rb index a98241f7a..96ac4a125 100644 --- a/spec/models/core_workflow_spec.rb +++ b/spec/models/core_workflow_spec.rb @@ -207,6 +207,24 @@ RSpec.describe CoreWorkflow, type: :model do expect(result[:restrict_values]['owner_id']).to eq(['', action_user.id.to_s]) end end + + describe 'Ticket owner selection is not updated if owner selection should be empty #3809' do + let(:group_no_owners) { create(:group) } + let(:ticket2) { create(:ticket, group: group_no_owners) } + let(:payload) do + base_payload.merge('screen' => 'overview_bulk', 'params' => { 'ticket_ids' => ticket2.id.to_s }) + end + + before do + action_user.group_names_access_map = { + group_no_owners.name => %w[create read change overview], + } + end + + it 'does not show any owners for group with no full permitted users' do + expect(result[:restrict_values]['owner_id']).to eq(['']) + end + end end describe '.perform - Default - State' do @@ -359,7 +377,7 @@ RSpec.describe CoreWorkflow, type: :model do base_payload.merge( 'screen' => 'edit', 'class_name' => 'Sla', - 'params' => { 'update_time_enabled' => 'true' } + 'params' => { 'update_time_enabled' => 'true', 'update_type' => 'update' } ) end @@ -1717,4 +1735,119 @@ RSpec.describe CoreWorkflow, type: :model do end end end + + describe 'Ticket owner selection is not updated if owner selection should be empty #3809' do + let(:group_no_owners) { create(:group) } + let(:payload) do + base_payload.merge('params' => { 'group_id' => group_no_owners.id }) + end + + before do + action_user.group_names_access_map = { + group_no_owners.name => %w[create read change overview], + } + end + + it 'does not show any owners because no one has full permissions' do + expect(result[:restrict_values]['owner_id']).to eq(['']) + end + end + + describe 'Add clear selection action or has changed condition #3821' do + let!(:workflow_has_changed) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.priority_id': { + operator: 'has_changed', + }, + }) + end + let!(:workflow_changed_to) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.priority_id': { + operator: 'changed_to', + value: [ Ticket::Priority.find_by(name: '3 high').id.to_s ] + }, + }) + end + + context 'when priority changed' do + let(:payload) do + base_payload.merge('last_changed_attribute' => 'priority_id', 'params' => { 'priority_id' => Ticket::Priority.find_by(name: '3 high').id.to_s }) + end + + it 'does match on condition has changed' do + expect(result[:matched_workflows]).to include(workflow_has_changed.id) + end + + it 'does match on condition changed to' do + expect(result[:matched_workflows]).to include(workflow_changed_to.id) + end + end + + context 'when nothing changed' do + it 'does not match on condition has changed' do + expect(result[:matched_workflows]).not_to include(workflow_has_changed.id) + end + + it 'does not match on condition changed to' do + expect(result[:matched_workflows]).not_to include(workflow_changed_to.id) + end + end + + context 'when state changed' do + let(:payload) do + base_payload.merge('last_changed_attribute' => 'state_id') + end + + it 'does not match on condition has changed' do + expect(result[:matched_workflows]).not_to include(workflow_has_changed.id) + end + + it 'does not match on condition changed to' do + expect(result[:matched_workflows]).not_to include(workflow_changed_to.id) + end + end + end + + describe 'If selected value is not part of the restriction of set_fixed_to it should recalculate it with the new value #3822', db_strategy: :reset do + let(:field_name1) { SecureRandom.uuid } + let(:screens) do + { + 'create_middle' => { + 'ticket.agent' => { + 'shown' => false, + 'required' => false, + } + } + } + end + let!(:workflow1) do + create(:core_workflow, + object: 'Ticket', + perform: { "ticket.#{field_name1}" => { 'operator' => 'set_fixed_to', 'set_fixed_to' => ['key_3'] } }) + end + let!(:workflow2) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + "ticket.#{field_name1}": { + operator: 'is', + value: 'key_3', + }, + }) + end + + before do + create :object_manager_attribute_select, name: field_name1, display: field_name1, screens: screens + ObjectManager::Attribute.migration_execute + end + + it 'does select key_3 as new param value and based on this executes workflow 2' do + expect(result[:matched_workflows]).to include(workflow1.id, workflow2.id) + end + end end diff --git a/spec/models/data_privacy_task_spec.rb b/spec/models/data_privacy_task_spec.rb index 2c6a5f63b..a20ff7faa 100644 --- a/spec/models/data_privacy_task_spec.rb +++ b/spec/models/data_privacy_task_spec.rb @@ -36,18 +36,11 @@ RSpec.describe DataPrivacyTask, type: :model do expect(create(:data_privacy_task, deletable: admin)).to be_truthy end - it 'sets the failed state when task failed' do + it 'sets no error message when user is already deleted' do task = create(:data_privacy_task, deletable: user) user.destroy task.perform - expect(task.reload.state).to eq('failed') - end - - it 'sets an error message when task failed' do - task = create(:data_privacy_task, deletable: user) - user.destroy - task.perform - expect(task.reload.preferences[:error]).to eq("ERROR: #") + expect(task.reload.state).to eq('completed') end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 4111da026..42fb672d4 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -5,7 +5,6 @@ require 'models/application_model_examples' require 'models/concerns/can_be_imported_examples' require 'models/concerns/has_object_manager_attributes_examples' require 'models/concerns/has_collection_update_examples' -require 'models/concerns/has_ticket_create_screen_impact_examples' require 'models/concerns/has_xss_sanitized_note_examples' RSpec.describe Group, type: :model do @@ -15,6 +14,5 @@ RSpec.describe Group, type: :model do it_behaves_like 'CanBeImported' it_behaves_like 'HasObjectManagerAttributes' it_behaves_like 'HasCollectionUpdate', collection_factory: :group - it_behaves_like 'HasTicketCreateScreenImpact', create_screen_factory: :group it_behaves_like 'HasXssSanitizedNote', model_factory: :group end diff --git a/spec/models/object_manager/attribute/set_defaults_spec.rb b/spec/models/object_manager/attribute/set_defaults_spec.rb index 9e5a1cd3e..7fb9a98fe 100644 --- a/spec/models/object_manager/attribute/set_defaults_spec.rb +++ b/spec/models/object_manager/attribute/set_defaults_spec.rb @@ -2,18 +2,21 @@ require 'rails_helper' +DEFAULT_VALUES = { + text: 'rspec', + boolean: true, + date: 1, + datetime: 12, + integer: 123, + select: 'key_1' +}.freeze + RSpec.describe ObjectManager::Attribute::SetDefaults, type: :model do describe 'setting default', db_strategy: :reset_all do before :all do # rubocop:disable RSpec/BeforeAfterAll - { - text: 'rspec', - boolean: true, - date: 1, - datetime: 12, - integer: 123, - select: 'key_1' - }.each do |key, value| + DEFAULT_VALUES.each do |key, value| create("object_manager_attribute_#{key}", name: "rspec_#{key}", default: value) + create("object_manager_attribute_#{key}", name: "rspec_#{key}_no_default", default: nil) end create('object_manager_attribute_text', name: 'rspec_empty', default: '') @@ -77,7 +80,7 @@ RSpec.describe ObjectManager::Attribute::SetDefaults, type: :model do end it 'datetime is set' do - freeze_time + travel_to Time.current.change(usec: 0, sec: 0) expect(example.rspec_datetime).to eq 12.hours.from_now end @@ -89,5 +92,28 @@ RSpec.describe ObjectManager::Attribute::SetDefaults, type: :model do expect(example.rspec_select).to eq 'key_1' end end + + context 'when overriding default to empty value' do + subject(:example) do + params = DEFAULT_VALUES.keys.each_with_object({}) { |elem, memo| memo["rspec_#{elem}"] = nil } + create :ticket, params + end + + DEFAULT_VALUES.each_key do |elem| + it "#{elem} is empty" do + expect(example.send("rspec_#{elem}")).to be_nil + end + end + end + + context 'when default is not set' do + subject(:example) { create :ticket } + + DEFAULT_VALUES.each_key do |elem| + it "#{elem} is empty" do + expect(example.send("rspec_#{elem}_no_default")).to be_nil + end + end + end end end diff --git a/spec/models/object_manager/attribute_spec.rb b/spec/models/object_manager/attribute_spec.rb index 4e71f5b58..17db0da5c 100644 --- a/spec/models/object_manager/attribute_spec.rb +++ b/spec/models/object_manager/attribute_spec.rb @@ -76,6 +76,22 @@ RSpec.describe ObjectManager::Attribute, type: :model do end end + %w[title tags number].each do |not_editable_attribute| + it "rejects '#{not_editable_attribute}' which is used" do + expect do + described_class.add attributes_for :object_manager_attribute_text, name: not_editable_attribute + end.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Name Attribute not editable!') + end + end + + %w[priority state note].each do |existing_attribute| + it "rejects '#{existing_attribute}' which is used" do + expect do + described_class.add attributes_for :object_manager_attribute_text, name: existing_attribute + end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Name #{existing_attribute} already exists!") + end + end + it 'rejects duplicate attribute name of conflicting types' do attribute = attributes_for :object_manager_attribute_text described_class.add attribute diff --git a/spec/models/role_spec.rb b/spec/models/role_spec.rb index d5a67cf83..dee3680c2 100644 --- a/spec/models/role_spec.rb +++ b/spec/models/role_spec.rb @@ -5,7 +5,6 @@ require 'models/application_model_examples' require 'models/concerns/can_be_imported_examples' require 'models/concerns/has_groups_examples' require 'models/concerns/has_collection_update_examples' -require 'models/concerns/has_ticket_create_screen_impact_examples' require 'models/concerns/has_xss_sanitized_note_examples' RSpec.describe Role do @@ -15,7 +14,6 @@ RSpec.describe Role do it_behaves_like 'CanBeImported' it_behaves_like 'HasGroups', group_access_factory: :role it_behaves_like 'HasCollectionUpdate', collection_factory: :role - it_behaves_like 'HasTicketCreateScreenImpact', create_screen_factory: :role it_behaves_like 'HasXssSanitizedNote', model_factory: :role describe 'Default state' do diff --git a/spec/models/sla/has_escalation_calculation_impact_examples.rb b/spec/models/sla/has_escalation_calculation_impact_examples.rb index 4f390b6c1..de8b222ce 100644 --- a/spec/models/sla/has_escalation_calculation_impact_examples.rb +++ b/spec/models/sla/has_escalation_calculation_impact_examples.rb @@ -11,7 +11,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do context 'when affected Ticket existed' do - subject(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) } + subject(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) } let(:calendar) { create(:calendar, :business_hours_9_17) } let!(:ticket) { create(:ticket) } @@ -78,7 +78,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do }, }, first_response_time: 10, - update_time: 20, + response_time: 20, solution_time: 300) end @@ -92,7 +92,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do }, }, first_response_time: 120, - update_time: 180, + response_time: 180, solution_time: 240) end @@ -130,7 +130,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do }, calendar: calendar, first_response_time: 60, - update_time: 120, + response_time: 120, solution_time: 180) end @@ -172,7 +172,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do }, calendar: calendar, first_response_time: 60, - update_time: 120, + response_time: 120, solution_time: 180) end @@ -214,7 +214,7 @@ RSpec.shared_examples 'HasEscalationCalculationImpact', :performs_jobs do }, calendar: calendar, first_response_time: 60, - update_time: 120, + response_time: 120, solution_time: 180) end diff --git a/spec/models/sla_spec.rb b/spec/models/sla_spec.rb index 8b9d1dbe0..d84e81beb 100644 --- a/spec/models/sla_spec.rb +++ b/spec/models/sla_spec.rb @@ -35,6 +35,28 @@ RSpec.describe Sla, type: :model do end end + describe '#cannot_have_response_and_update' do + it 'allows neither #response_time nor #update_time' do + instance = build(:sla, response_time: nil, update_time: nil) + expect(instance).to be_valid + end + + it 'allows #response_time' do + instance = build(:sla, response_time: 180, update_time: nil) + expect(instance).to be_valid + end + + it 'allows #update_time' do + instance = build(:sla, response_time: nil, update_time: 180) + expect(instance).to be_valid + end + + it 'denies both #response_time and #update_time' do + instance = build(:sla, response_time: 180, update_time: 180) + expect(instance).not_to be_valid + end + end + describe '.for_ticket' do it 'returns matching SLA for the ticket' do sla @@ -56,6 +78,20 @@ RSpec.describe Sla, type: :model do sla_blank expect(described_class.for_ticket(ticket_matching)).to eq sla end + + context 'when multiple SLAs are matching' do + let(:sla) { create(:sla, :condition_title, condition_title: 'matching', name: 'ZZZ 1') } + let(:sla2) { create(:sla, :condition_title, condition_title: 'matching', name: 'AAA 1') } + + before do + sla + sla2 + end + + it 'returns the AAA 1 sla as matching' do + expect(described_class.for_ticket(ticket_matching)).to eq sla2 + end + end end end end diff --git a/spec/models/ticket/article/adds_metadata_general_spec.rb b/spec/models/ticket/article/adds_metadata_general_spec.rb index 79b2c6d5f..3143851c3 100644 --- a/spec/models/ticket/article/adds_metadata_general_spec.rb +++ b/spec/models/ticket/article/adds_metadata_general_spec.rb @@ -31,4 +31,24 @@ RSpec.describe Ticket::Article::AddsMetadataGeneral do end end end + + context 'when Agent-Customer in shared organization creates Article' do + let(:organization) { create(:organization, shared: true) } + let(:agent_a) { create(:agent_and_customer, organization: organization) } + let(:agent_b) { create(:agent_and_customer, organization: organization) } + let(:group) { create(:group) } + let(:ticket) { create(:ticket, group: group, owner: agent_a, customer: agent_b) } + + before do + [agent_a, agent_b].each do |elem| + elem.user_groups.create group: group, access: 'create' + end + end + + it '#origin_by is set correctly', current_user_id: -> { agent_a.id } do + article = create(:ticket_article, :inbound_web, ticket: ticket) + + expect(article.origin_by).to be_nil + end + end end diff --git a/spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb b/spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb index 5d80c3fdd..7ae7f0f59 100644 --- a/spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb +++ b/spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb @@ -39,7 +39,7 @@ RSpec.shared_examples 'Ticket::Article::HasTicketContactAttributesImpact' do }, }) end - let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: 180) } + let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: 180) } before do sla @@ -174,7 +174,7 @@ RSpec.shared_examples 'Ticket::Article::HasTicketContactAttributesImpact' do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: nil) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: nil) } before do sla @@ -252,7 +252,7 @@ RSpec.shared_examples 'Ticket::Article::HasTicketContactAttributesImpact' do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: nil) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: nil) } before do Setting.set('ticket_last_contact_behaviour', 'based_on_customer_reaction') diff --git a/spec/models/ticket/escalation_examples.rb b/spec/models/ticket/escalation_examples.rb index a1bd2388e..c74d5ff96 100644 --- a/spec/models/ticket/escalation_examples.rb +++ b/spec/models/ticket/escalation_examples.rb @@ -41,7 +41,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }, }) end - let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 120, solution_time: 180) } + let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: 180) } let(:article) { create(:'ticket/article', :inbound_email, ticket: ticket, created_at: '2013-03-21 09:30:00 UTC', updated_at: '2013-03-21 09:30:00 UTC') } before do @@ -434,7 +434,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }, }, first_response_time: 60, - update_time: 180, + response_time: 180, solution_time: 240) end @@ -512,7 +512,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }) end - let(:sla) { create(:sla, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 250) } + let(:sla) { create(:sla, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 250) } context 'when Ticket is reopened' do @@ -663,7 +663,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 250) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 250) } before do sla @@ -774,7 +774,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 240) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 240) } before do sla @@ -851,7 +851,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 240) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 240) } before do sla @@ -950,7 +950,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 180, solution_time: 240) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 180, solution_time: 240) } before do sla @@ -1068,7 +1068,7 @@ RSpec.shared_examples 'Ticket::Escalation' do }) end - let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, update_time: 1200, solution_time: nil) } + let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 120, response_time: 1200, solution_time: nil) } before do sla diff --git a/spec/models/ticket_spec.rb b/spec/models/ticket_spec.rb index 748e18849..c3696d7f0 100644 --- a/spec/models/ticket_spec.rb +++ b/spec/models/ticket_spec.rb @@ -1167,7 +1167,7 @@ RSpec.describe Ticket, type: :model do describe '#escalation_at' do before { travel_to(Time.current) } # freeze time - let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) } + let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) } let(:calendar) { create(:calendar, :'24/7') } context 'with no SLAs in the system' do @@ -1378,7 +1378,7 @@ RSpec.describe Ticket, type: :model do describe '#first_response_escalation_at' do before { travel_to(Time.current) } # freeze time - let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) } + let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) } let(:calendar) { create(:calendar, :'24/7') } context 'with no SLAs in the system' do @@ -1410,7 +1410,7 @@ RSpec.describe Ticket, type: :model do describe '#update_escalation_at' do before { travel_to(Time.current) } # freeze time - let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) } + let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) } let(:calendar) { create(:calendar, :'24/7') } context 'with no SLAs in the system' do @@ -1450,7 +1450,7 @@ RSpec.describe Ticket, type: :model do describe '#close_escalation_at' do before { travel_to(Time.current) } # freeze time - let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) } + let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, response_time: 180, solution_time: 240) } let(:calendar) { create(:calendar, :'24/7') } context 'with no SLAs in the system' do diff --git a/spec/models/transaction/notification_spec.rb b/spec/models/transaction/notification_spec.rb index 7d034314c..7d285dca3 100644 --- a/spec/models/transaction/notification_spec.rb +++ b/spec/models/transaction/notification_spec.rb @@ -31,7 +31,77 @@ RSpec.describe Transaction::Notification, type: :model do end end + describe '#ooo_replacements' do + subject(:notification_instance) { build(ticket, user) } + + let(:group) { create(:group) } + let(:user) { create(:agent, :ooo, :groupable, ooo_agent: replacement_1, group: group) } + let(:ticket) { create(:ticket, owner: user, group: group, state_name: 'open', pending_time: Time.current) } + + context 'when replacement has access' do + let(:replacement_1) { create(:agent, :groupable, group: group) } + + it 'is added to list' do + replacements = Set.new + + ooo(notification_instance, user, replacements: replacements) + + expect(replacements).to include replacement_1 + end + + context 'when replacement has replacement' do + let(:replacement_1) { create(:agent, :ooo, :groupable, ooo_agent: replacement_2, group: group) } + let(:replacement_2) { create(:agent, :groupable, group: group) } + + it 'replacement\'s replacement added to list' do + replacements = Set.new + + ooo(notification_instance, user, replacements: replacements) + + expect(replacements).to include replacement_2 + end + + it 'intermediary replacement is not in list' do + replacements = Set.new + + ooo(notification_instance, user, replacements: replacements) + + expect(replacements).not_to include replacement_1 + end + end + end + + context 'when replacement does not have access' do + let(:replacement_1) { create(:agent) } + + it 'is not added to list' do + replacements = Set.new + + ooo(notification_instance, user, replacements: replacements) + + expect(replacements).not_to include replacement_1 + end + + context 'when replacement has replacement with access' do + let(:replacement_1) { create(:agent, :ooo, ooo_agent: replacement_2) } + let(:replacement_2) { create(:agent, :groupable, group: group) } + + it 'his replacement may be added' do + replacements = Set.new + + ooo(notification_instance, user, replacements: replacements) + + expect(replacements).to include replacement_2 + end + end + end + end + def run(ticket, user, type) + build(ticket, user, type).perform + end + + def build(ticket, user, type = 'reminder_reached') described_class.new( object: ticket.class.name, type: type, @@ -40,6 +110,10 @@ RSpec.describe Transaction::Notification, type: :model do changes: nil, created_at: Time.current, user_id: user.id - ).perform + ) + end + + def ooo(instance, user, replacements: Set.new, reasons: []) + instance.send(:ooo_replacements, user: user, replacements: replacements, ticket: ticket, reasons: reasons) end end diff --git a/spec/models/user/has_ticket_create_screen_impact_examples.rb b/spec/models/user/has_ticket_create_screen_impact_examples.rb deleted file mode 100644 index 627532e75..000000000 --- a/spec/models/user/has_ticket_create_screen_impact_examples.rb +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ - -RSpec.shared_examples 'User::HasTicketCreateScreenImpact' do - - describe '#push_ticket_create_screen', performs_jobs: true do - shared_examples 'relevant User Role' do |role| - - context "relevant User Role is '#{role}'" do - - subject { create(:user, roles: Role.where(name: role)) } - - context 'creating a record' do - it 'enqueues TicketCreateScreenJob' do - expect { subject }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'record exists' do - - before do - subject - clear_jobs - end - - context 'attribute updated' do - it 'enqueues TicketCreateScreenJob' do - expect { subject.update!(firstname: 'new firstname') }.to have_enqueued_job(TicketCreateScreenJob) - end - - context 'permission association changes' do - - context 'Group' do - - let!(:group) { create(:group) } - - before { clear_jobs } - - it 'enqueues TicketCreateScreenJob' do - expect do - subject.group_names_access_map = { - group.name => ['full'], - } - end.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'Role' do - context 'to relevant' do - - let!(:roles) { create_list(:agent_role, 1) } - - before { clear_jobs } - - it 'enqueues TicketCreateScreenJob' do - expect { subject.update!(roles: roles) }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'to irrelevant' do - - let!(:roles) { create_list(:role, 3) } - - before { clear_jobs } - - it 'enqueues TicketCreateScreenJob' do - expect { subject.update!(roles: roles) }.to have_enqueued_job(TicketCreateScreenJob) - end - end - end - end - end - - context 'record is deleted' do - it 'enqueues TicketCreateScreenJob' do - expect { subject.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - end - end - end - end - - shared_examples 'irrelevant User Role' do |role| - - context "irrelevant User Role is '#{role}'" do - - subject { create(:user, roles: Role.where(name: role)) } - - context 'creating a record' do - it 'does not enqueue TicketCreateScreenJob job' do - expect { subject }.not_to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'record exists' do - - before do - subject - clear_jobs - end - - context 'attribute updated' do - it 'enqueues no TicketCreateScreenJob job' do - expect { subject.update!(firstname: 'new firstname') }.not_to have_enqueued_job(TicketCreateScreenJob) - end - - context 'permission association changes', last_admin_check: false do - - context 'Group' do - - let!(:group) { create(:group) } - - before { clear_jobs } - - it 'enqueues TicketCreateScreenJob' do - expect do - subject.group_names_access_map = { - group.name => ['full'], - } - end.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'Role' do - - context 'to relevant' do - - let!(:roles) { create_list(:agent_role, 3) } - - before { clear_jobs } - - it 'enqueues TicketCreateScreenJob' do - expect { subject.update!(roles: roles) }.to have_enqueued_job(TicketCreateScreenJob) - end - end - - context 'to irrelevant' do - - let!(:roles) { create_list(:role, 3) } - - before { clear_jobs } - - it 'does not enqueue TicketCreateScreenJob' do - expect { subject.update!(roles: roles) }.not_to have_enqueued_job(TicketCreateScreenJob) - end - end - end - end - end - - context 'record is deleted' do - it 'enqueues TicketCreateScreenJob job' do - expect { subject.destroy! }.to have_enqueued_job(TicketCreateScreenJob) - end - end - end - end - end - - include_examples 'relevant User Role', 'Agent' - include_examples 'irrelevant User Role', 'Customer' - include_examples 'irrelevant User Role', 'Admin' - end -end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6bbc6eca7..e1562f49f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -10,7 +10,6 @@ require 'models/concerns/has_xss_sanitized_note_examples' require 'models/concerns/can_be_imported_examples' require 'models/concerns/has_object_manager_attributes_examples' require 'models/user/can_lookup_search_index_attributes_examples' -require 'models/user/has_ticket_create_screen_impact_examples' require 'models/user/performs_geo_lookup_examples' require 'models/concerns/has_taskbars_examples' @@ -29,7 +28,6 @@ RSpec.describe User, type: :model do it_behaves_like 'HasGroups and Permissions', group_access_no_permission_factory: :user it_behaves_like 'CanBeImported' it_behaves_like 'HasObjectManagerAttributes' - it_behaves_like 'User::HasTicketCreateScreenImpact' it_behaves_like 'CanLookupSearchIndexAttributes' it_behaves_like 'HasTaskbars' it_behaves_like 'UserPerformsGeoLookup' @@ -250,6 +248,62 @@ RSpec.describe User, type: :model do expect(user.out_of_office_agent).to eq(substitute) end end + + context 'with stack depth exceeding limit' do + let(:replacement_chain) do + user = create(:agent) + + 14 + .times + .each_with_object([user]) do |_, memo| + memo << create(:agent, :ooo, ooo_agent: memo.last) + end + .reverse + end + + let(:ids_executed) { [] } + + before do + allow_any_instance_of(described_class).to receive(:out_of_office_agent).and_wrap_original do |method, *args| # rubocop:disable RSpec/AnyInstance + ids_executed << method.receiver.id + method.call(*args) + end + + allow(Rails.logger).to receive(:warn) + end + + it 'returns the last agent at the limit' do + expect(replacement_chain.first.out_of_office_agent).to eq replacement_chain[10] + end + + it 'does not evaluate element beyond the limit' do + user_beyond_limit = replacement_chain[11] + + replacement_chain.first.out_of_office_agent + + expect(ids_executed).not_to include(user_beyond_limit.id) + end + + it 'does evaluate element within the limit' do + user_within_limit = replacement_chain[5] + + replacement_chain.first.out_of_office_agent + + expect(ids_executed).to include(user_within_limit.id) + end + + it 'logs error below the limit' do + replacement_chain.first.out_of_office_agent + + expect(Rails.logger).to have_received(:warn).with(%r{#{Regexp.escape('Found more than 10 replacement levels for agent')}}) + end + + it 'does not logs warn within the limit' do + replacement_chain[10].out_of_office_agent + + expect(Rails.logger).not_to have_received(:warn) + end + end end end diff --git a/spec/requests/calendar_spec.rb b/spec/requests/calendar_spec.rb index 023ae25d3..64c538de0 100644 --- a/spec/requests/calendar_spec.rb +++ b/spec/requests/calendar_spec.rb @@ -76,4 +76,18 @@ RSpec.describe 'Calendars', type: :request do end end + describe 'Removing calendars via UI and API does not check for references #3845', authenticated_as: -> { user } do + let(:calendar) { create(:calendar) } + let(:sla) { create(:sla, calendar: calendar) } + let(:user) { create(:admin) } + + before do + sla + end + + it 'does return reference error on delete if related objects exist' do + delete "/api/v1/calendars/#{calendar.id}", params: {}, as: :json + expect(json_response['error']).to eq("Can't delete, object has references.") + end + end end diff --git a/spec/requests/group_spec.rb b/spec/requests/group_spec.rb new file mode 100644 index 000000000..4e3bef8a1 --- /dev/null +++ b/spec/requests/group_spec.rb @@ -0,0 +1,20 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +require 'rails_helper' + +RSpec.describe 'Group', type: :request, authenticated_as: -> { user } do + describe 'Zammad returns stack error when one tries to remove groups via API #3841' do + let(:group) { create(:group) } + let(:ticket) { create(:ticket, group: group) } + let(:user) { create(:admin) } + + before do + ticket + end + + it 'does return reference error on delete if related objects exist' do + delete "/api/v1/groups/#{group.id}", params: {}, as: :json + expect(json_response['error']).to eq("Can't delete, object has references.") + end + end +end diff --git a/spec/requests/integration/object_manager_attributes_spec.rb b/spec/requests/integration/object_manager_attributes_spec.rb index f53e1a901..9c5c8cc13 100644 --- a/spec/requests/integration/object_manager_attributes_spec.rb +++ b/spec/requests/integration/object_manager_attributes_spec.rb @@ -1039,7 +1039,7 @@ RSpec.describe 'ObjectManager Attributes', type: :request do end context 'position handling', authenticated_as: -> { admin } do - let(:params) do + let(:base_params) do { name: "customerdescription_#{SecureRandom.uuid.tr('-', '_')}", object: 'Ticket', @@ -1060,15 +1060,35 @@ RSpec.describe 'ObjectManager Attributes', type: :request do before { post '/api/v1/object_manager_attributes', params: params, as: :json } context 'when creating a new attribute' do - it 'defaults to 1550' do - expect(new_attribute_object.position).to eq 1550 + let(:params) { base_params } + + context 'with no position attribute provided' do + let(:maximum_position) do + ObjectManager::Attribute + .for_object(params[:object]) + .maximum(:position) + end + + it 'defaults to the maximum available position' do + expect(new_attribute_object.position).to eq maximum_position + end + end + + context 'with a position attribute given' do + let(:position) { 50 } + let(:params) { base_params.merge(position: position) } + + it 'defaults to given position' do + expect(new_attribute_object.position).to eq position + end end end context 'when updating an existing attribute' do let(:alternative_position) { 123 } let(:alternative_display) { 'another description' } - let(:alternative_params) { params.deep_dup.update(display: alternative_display) } + let(:params) { base_params } + let(:alternative_params) { base_params.merge(display: alternative_display) } before do new_attribute_object.update! position: alternative_position diff --git a/spec/requests/integration/placetel_spec.rb b/spec/requests/integration/placetel_spec.rb index 19fa920dc..cb3843751 100644 --- a/spec/requests/integration/placetel_spec.rb +++ b/spec/requests/integration/placetel_spec.rb @@ -569,8 +569,8 @@ RSpec.describe 'Integration Placetel', type: :request do config[:outbound][:default_caller_id] = '' Setting.set('placetel_config', config) - stub_request(:post, 'https://api.placetel.de/api/getVoIPUsers.json') - .to_return(status: 200, body: [{ 'callerid' => '03055571600', 'did' => 10, 'name' => 'Bob Smith', 'stype' => 3, 'uid' => '777008478072@example.com', 'uid2' => nil }, { 'callerid' => '03055571600', 'did' => 12, 'name' => 'Josef Müller', 'stype' => 3, 'uid' => '777042617425@example.com', 'uid2' => nil }].to_json) + stub_request(:get, 'https://api.placetel.de/v2/sip_users') + .to_return(status: 200, body: [{ 'callerid' => '03055571600', 'did' => 10, 'name' => 'Bob Smith', 'type' => 'standard', 'sipuid' => '777008478072@example.com' }, { 'callerid' => '03055571600', 'did' => 12, 'name' => 'Josef Müller', 'type' => 'standard', 'sipuid' => '777042617425@example.com' }].to_json) params = 'event=OutgoingCall&direction=out&to=099999222222&call_id=1234567890-2&from=777008478072@example.com' post "/api/v1/placetel/#{token}", params: params diff --git a/spec/requests/ticket/escalation_spec.rb b/spec/requests/ticket/escalation_spec.rb index da71adec5..a5866bc2c 100644 --- a/spec/requests/ticket/escalation_spec.rb +++ b/spec/requests/ticket/escalation_spec.rb @@ -4,8 +4,8 @@ require 'rails_helper' RSpec.describe 'Ticket Escalation', type: :request do let(:sla_first_response) { 1.hour } - let(:sla_update) { 3.hours } - let(:sla_close) { 4.hours } + let(:sla_response) { 3.hours } + let(:sla_close) { 4.hours } let!(:mail_group) { create(:group, email_address: create(:email_address)) } @@ -14,7 +14,7 @@ RSpec.describe 'Ticket Escalation', type: :request do create(:sla, calendar: calendar, first_response_time: sla_first_response / 1.minute, - update_time: sla_update / 1.minute, + response_time: sla_response / 1.minute, solution_time: sla_close / 1.minute) end diff --git a/spec/support/capybara/authenticated.rb b/spec/support/capybara/authenticated.rb index 020ffabe2..bf60a0bf3 100644 --- a/spec/support/capybara/authenticated.rb +++ b/spec/support/capybara/authenticated.rb @@ -12,12 +12,28 @@ RSpec.configure do |config| config.before(:each, type: :system) do |example| + ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil + # there is no way to authenticated in a not set up system next if !example.metadata.fetch(:set_up, true) authenticated = example.metadata.fetch(:authenticated_as, true) - credentials = authenticated_as_get_user(authenticated, return_type: :credentials) + credentials = authenticated_as_get_user(authenticated, return_type: :credentials) - login(**credentials) if credentials + authentication_type = example.metadata.fetch(:authentication_type, :auto) + + next if credentials.nil? + + if authentication_type == :form + login(**credentials) + else + ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = User.find_by(email: credentials[:username]).id.to_s + + visit '/' + + wait(4).until_exists do + current_login + end + end end end diff --git a/spec/support/capybara/browser_test_helper.rb b/spec/support/capybara/browser_test_helper.rb index 241662591..74cb19154 100644 --- a/spec/support/capybara/browser_test_helper.rb +++ b/spec/support/capybara/browser_test_helper.rb @@ -75,7 +75,7 @@ module BrowserTestHelper end wait(5, interval: 0.1).until_constant do - page.evaluate_script('App.Ajax.queue().length').zero? + page.evaluate_script('App.Ajax.queue().length').zero? && page.evaluate_script('Object.keys(App.FormHandlerCoreWorkflow.getRequests()).length').zero? end rescue nil diff --git a/spec/support/capybara/common_actions.rb b/spec/support/capybara/common_actions.rb index d9e640221..6405961af 100644 --- a/spec/support/capybara/common_actions.rb +++ b/spec/support/capybara/common_actions.rb @@ -22,6 +22,8 @@ module CommonActions # # return [nil] def login(username:, password:, remember_me: false) + ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil + visit '/' within('#login') do @@ -91,6 +93,7 @@ module CommonActions # logout # def logout + ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil visit('logout') end diff --git a/spec/support/capybara/selenium_driver.rb b/spec/support/capybara/selenium_driver.rb index ea016806a..cfe5f6804 100644 --- a/spec/support/capybara/selenium_driver.rb +++ b/spec/support/capybara/selenium_driver.rb @@ -29,6 +29,8 @@ Capybara.register_driver(:zammad_chrome) do |app| options[:url] = ENV['REMOTE_URL'] end + ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil + Capybara::Selenium::Driver.new(app, **options) end @@ -54,5 +56,7 @@ Capybara.register_driver(:zammad_firefox) do |app| options[:url] = ENV['REMOTE_URL'] end + ENV['FAKE_SELENIUM_LOGIN_USER_ID'] = nil + Capybara::Selenium::Driver.new(app, **options) end diff --git a/spec/system/channels/email_spec.rb b/spec/system/channels/email_spec.rb index c17662b37..ce42a0551 100644 --- a/spec/system/channels/email_spec.rb +++ b/spec/system/channels/email_spec.rb @@ -54,8 +54,9 @@ RSpec.describe 'Manage > Channels > Email', type: :system do expect(find('input[name="options::port"]').value).to eq('143') port = '4242' fill_in 'options::port', with: port + field.click expect(find('input[name="options::port"]').value).to eq(port) - option_yes.select_option + fill_in 'options::folder', with: 'testabc' expect(find('input[name="options::port"]').value).to eq(port) click '.js-close' end diff --git a/spec/system/chat_spec.rb b/spec/system/chat_spec.rb index 3dbaad633..6cdef49f9 100644 --- a/spec/system/chat_spec.rb +++ b/spec/system/chat_spec.rb @@ -388,4 +388,29 @@ RSpec.describe 'Chat Handling', type: :system do include_examples 'chat button is hidden after idle timeout' end end + + describe "Chat can't be closed after timeout #2471", authenticated_as: :authenticate do + shared_examples 'test issue #2471' do + it 'is able to close to the dialog after a idleTimeout happened' do + click agent_chat_switch_selector + open_window_and_switch + + visit chat_url + click '.zammad-chat .js-chat-open' + expect(page).to have_selector('.js-restart', wait: 60) + click '.js-chat-toggle .zammad-chat-header-icon' + expect(page).to have_no_selector('zammad-chat-is-open', wait: 60) + end + end + + context 'with jquery' do + include_examples 'test issue #2471' + end + + context 'wihtout jquery' do + let(:chat_url_type) { 'znuny-no-jquery' } + + include_examples 'test issue #2471' + end + end end diff --git a/spec/system/import/freshdesk_spec.rb b/spec/system/import/freshdesk_spec.rb index e5fc58ba6..f0771b515 100644 --- a/spec/system/import/freshdesk_spec.rb +++ b/spec/system/import/freshdesk_spec.rb @@ -112,7 +112,13 @@ RSpec.describe 'Import Freshdesk', type: :system, set_up: false, authenticated_a Rake::Task['zammad:setup:auto_wizard'].execute - expect(page).to have_text('Login') + expect(page).to have_text(Setting.get('fqdn')) + + # Check that the login is working and also the left navigation side bar is visible. + login( + username: 'admin@example.com', + password: 'test', + ) end end end diff --git a/spec/system/import/zendesk_spec.rb b/spec/system/import/zendesk_spec.rb index 40058452e..f9db78fdb 100644 --- a/spec/system/import/zendesk_spec.rb +++ b/spec/system/import/zendesk_spec.rb @@ -121,7 +121,13 @@ RSpec.describe 'Import Zendesk', type: :system, set_up: false, authenticated_as: Rake::Task['zammad:setup:auto_wizard'].execute - expect(page).to have_text('Login') + expect(page).to have_text(Setting.get('fqdn')) + + # Check that the login is working and also the left navigation side bar is visible. + login( + username: 'admin@example.com', + password: 'test', + ) end end end diff --git a/spec/system/js/q_unit_spec.rb b/spec/system/js/q_unit_spec.rb index ffa1fe604..63b392edc 100644 --- a/spec/system/js/q_unit_spec.rb +++ b/spec/system/js/q_unit_spec.rb @@ -68,6 +68,10 @@ RSpec.describe 'QUnit', type: :system, authenticated_as: false, set_up: true, we it 'Image Service' do q_unit_tests('image_service') end + + it 'View helpers' do + q_unit_tests('view_helpers') + end end context 'Form' do diff --git a/spec/system/knowledge_base_public/editor_spec.rb b/spec/system/knowledge_base_public/editor_spec.rb index f215e8e2e..87d630ceb 100644 --- a/spec/system/knowledge_base_public/editor_spec.rb +++ b/spec/system/knowledge_base_public/editor_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -RSpec.describe 'Public Knowledge Base for editor', type: :system do +RSpec.describe 'Public Knowledge Base for editor', authentication_type: :form, type: :system do include_context 'basic Knowledge Base' before do diff --git a/spec/system/manage/sla_spec.rb b/spec/system/manage/sla_spec.rb index 385109e81..50945c1d5 100644 --- a/spec/system/manage/sla_spec.rb +++ b/spec/system/manage/sla_spec.rb @@ -14,6 +14,7 @@ RSpec.describe 'Manage > Sla', type: :system do # enable all checkboxes page.find('input#update_time', visible: false).find(:xpath, './/..').click + page.first('.js-updateTypeSelector', visible: false).click page.find('input#solution_time', visible: false).find(:xpath, './/..').click # check if required diff --git a/spec/system/manage/users_spec.rb b/spec/system/manage/users_spec.rb index fe10293a9..be2cd1e2d 100644 --- a/spec/system/manage/users_spec.rb +++ b/spec/system/manage/users_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe 'Manage > Users', type: :system do - describe 'switching to an alternative user', authenticated_as: -> { original_user } do + describe 'switching to an alternative user', authentication_type: :form, authenticated_as: -> { original_user } do let(:original_user) { create(:admin) } let(:alternative_one_user) { create(:admin) } let(:alternative_two_user) { create(:admin) } diff --git a/spec/system/online_notification_spec.rb b/spec/system/online_notification_spec.rb index ca65801f6..6ebe81e2f 100644 --- a/spec/system/online_notification_spec.rb +++ b/spec/system/online_notification_spec.rb @@ -15,6 +15,8 @@ RSpec.describe 'Online notification', type: :system do context 'when pending time is reached soon' do before do visit "ticket/zoom/#{ticket.id}" + + wait.until_exists { find("a[data-key='Ticket-#{ticket.id}']", wait: 0) } end let(:ticket) { create(:ticket, owner: session_user, group: Group.first, state_name: 'pending reminder', pending_time: 4.seconds.from_now) } @@ -44,6 +46,8 @@ RSpec.describe 'Online notification', type: :system do before do ensure_websocket do visit "ticket/zoom/#{ticket.id}" + + wait.until_exists { find("a[data-key='Ticket-#{ticket.id}']", wait: 0) } end ticket.update! state: Ticket::State.lookup(name: 'pending reminder'), pending_time: 5.seconds.from_now diff --git a/spec/system/overview_spec.rb b/spec/system/overview_spec.rb index 763619ec2..b255844c1 100644 --- a/spec/system/overview_spec.rb +++ b/spec/system/overview_spec.rb @@ -66,13 +66,19 @@ RSpec.describe 'Overview', type: :system do end end - context 'sorting when group by is set' do - let(:user) { create(:customer) } - let(:ticket1) { create(:ticket, group: Group.find_by(name: 'Users'), priority_id: 1, customer: user) } - let(:ticket2) { create(:ticket, group: Group.find_by(name: 'Users'), priority_id: 2, customer: user) } - let(:ticket3) { create(:ticket, group: Group.find_by(name: 'Users'), priority_id: 3, customer: user) } + context 'sorting when group by is set', authenticated_as: :user do + let(:user) { create(:agent, groups: [group_c, group_a, group_b]) } + + let(:group_a) { create(:group, name: 'aaa') } + let(:group_b) { create(:group, name: 'bbb') } + let(:group_c) { create(:group, name: 'ccc') } + + let(:ticket1) { create(:ticket, group: group_a, priority_id: 1, customer: user) } + let(:ticket2) { create(:ticket, group: group_c, priority_id: 2, customer: user) } + let(:ticket3) { create(:ticket, group: group_b, priority_id: 3, customer: user) } + let(:overview) do - create(:overview, group_by: 'priority', group_direction: group_direction, condition: { + create(:overview, group_by: group_key, group_direction: group_direction, condition: { 'ticket.customer_id' => { operator: 'is', value: user.id @@ -86,48 +92,60 @@ RSpec.describe 'Overview', type: :system do visit "ticket/view/#{overview.link}" end - context 'when group direction is default' do - let(:group_direction) { nil } + context 'when grouping by priority' do + let(:group_key) { 'priority' } - it 'sorts groups 1 > 3' do - within :active_content do - expect(find('.table-overview table tbody tr:first-child td:nth-child(1)').text).to match('1 low') - expect(find('.table-overview table tbody tr:nth-child(5) td:nth-child(1)').text).to match('3 high') + context 'when group direction is default' do + let(:group_direction) { nil } + + it 'sorts groups 1 > 3' do + within :active_content do + expect(all('.table-overview table b').map(&:text)).to eq ['1 low', '2 normal', '3 high'] + end + end + + it 'does not show duplicates when any ticket attribute is updated using bulk update' do + find("tr[data-id='#{ticket3.id}']").check('bulk', allow_label_click: true) + select '2 normal', from: 'priority_id' + + click '.js-confirm' + find('.js-confirm-step textarea').fill_in with: 'test tickets ordering' + click '.js-submit' + + within :active_content do + expect(page).to have_css("tr[data-id='#{ticket3.id}']", count: 1) + end end end - it 'does not show duplicates when any ticket attribute is updated using bulk update' do - find("tr[data-id='#{ticket3.id}']").check('bulk', allow_label_click: true) - select '2 normal', from: 'priority_id' + context 'when group direction is ASC' do + let(:group_direction) { 'ASC' } - click '.js-confirm' - find('.js-confirm-step textarea').fill_in with: 'test tickets ordering' - click '.js-submit' + it 'sorts groups 1 > 3' do + within :active_content do + expect(all('.table-overview table b').map(&:text)).to eq ['1 low', '2 normal', '3 high'] + end + end + end - within :active_content do - expect(page).to have_css("tr[data-id='#{ticket3.id}']", count: 1) + context 'when group direction is DESC' do + let(:group_direction) { 'DESC' } + + it 'sorts groups 3 > 1' do + within :active_content do + expect(all('.table-overview table b').map(&:text)).to eq ['3 high', '2 normal', '1 low'] + end end end end - context 'when group direction is ASC' do + context 'when grouping by groups' do + let(:group_key) { 'group' } let(:group_direction) { 'ASC' } - it 'sorts groups 1 > 3' do + it 'sorts groups a > b > c' do within :active_content do - expect(find('.table-overview table tbody tr:first-child td:nth-child(1)').text).to match('1 low') - expect(find('.table-overview table tbody tr:nth-child(5) td:nth-child(1)').text).to match('3 high') - end - end - end - - context 'when group direction is DESC' do - let(:group_direction) { 'DESC' } - - it 'sorts groups 3 > 1' do - within :active_content do - expect(find('.table-overview table tbody tr:first-child td:nth-child(1)').text).to match('3 high') - expect(find('.table-overview table tbody tr:nth-child(5) td:nth-child(1)').text).to match('1 low') + expect(all('.table-overview table b').map(&:text)).to eq %w[aaa bbb ccc] end end end diff --git a/spec/system/report_spec.rb b/spec/system/report_spec.rb index 447701b1d..234179ceb 100644 --- a/spec/system/report_spec.rb +++ b/spec/system/report_spec.rb @@ -33,4 +33,42 @@ RSpec.describe 'Report', type: :system, searchindex: true do end end end + + context 'report profiles are displayed' do + let!(:report_profile_active) { create(:report_profile) } + let!(:report_profile_inactive) { create(:report_profile, active: false) } + + it 'shows report profiles' do + visit 'report' + + expect(page) + .to have_css('ul.checkbox-list .label-text', text: report_profile_active.name) + .and have_no_css('ul.checkbox-list .label-text', text: report_profile_inactive.name) + end + end + + context 'with report profiles with date-based conditions' do + let(:report_profile) { create(:report_profile, :condition_created_at, ticket_created_at: 1.year.ago) } + + before do + freeze_time + report_profile + visit 'report' + end + + it 'shows previous year for a profile with matching conditions' do + click '.js-timePickerYear', text: Time.zone.now.year - 1 + click '.label-text', text: report_profile.name + + expect(page).to have_no_css('.modal') + end + + it 'throws error for a profile when showing a different year than described in the profile' do + click '.label-text', text: report_profile.name + + in_modal disappears: false do + expect(page).to have_text 'Conflicting date ranges' + end + end + end end diff --git a/spec/system/search_spec.rb b/spec/system/search_spec.rb index 5d3c7d6d4..652af5fc4 100644 --- a/spec/system/search_spec.rb +++ b/spec/system/search_spec.rb @@ -9,7 +9,7 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d let(:note) { 'Test note' } before do - configure_elasticsearch(required: true, rebuild: true) + ticket_1 && ticket_2 && configure_elasticsearch(required: true, rebuild: true) end it 'shows default widgets' do @@ -24,8 +24,6 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d context 'with ticket search result' do before do - ticket_1 && ticket_2 && rebuild_searchindex - fill_in id: 'global-search', with: 'Testing' click_on 'Show Search Details' @@ -176,4 +174,64 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d expect(page).to have_css('.popover-title .is-inactive', count: 1) end end + + describe 'Search is not triggered/updated if url of search is updated new search item or new search is triggered via global search #3873' do + + context 'when search changed via input box' do + before do + visit '#search' + end + + it 'does switch search results properly' do + page.find('.js-search').fill_in(with: '"Testing Ticket 1"', fill_options: { clear: :backspace }) + expect(page.find('.js-tableBody')).to have_text('Testing Ticket 1') + expect(page.find('.js-tableBody')).to have_no_text('Testing Ticket 2') + expect(current_url).to include('Testing%20Ticket%201') + + # switch by global search + page.find('.js-search').fill_in(with: '"Testing Ticket 2"', fill_options: { clear: :backspace }) + expect(page.find('.js-tableBody')).to have_text('Testing Ticket 2') + expect(page.find('.js-tableBody')).to have_no_text('Testing Ticket 1') + expect(current_url).to include('Testing%20Ticket%202') + end + end + + context 'when search changed via global search' do + before do + fill_in id: 'global-search', with: '"Testing Ticket 1"' + click_on 'Show Search Details' + end + + it 'does switch search results properly' do + expect(page.find('.js-tableBody')).to have_text('Testing Ticket 1') + expect(page.find('.js-tableBody')).to have_no_text('Testing Ticket 2') + expect(current_url).to include('Testing%20Ticket%201') + + # switch by global search + fill_in id: 'global-search', with: '"Testing Ticket 2"' + click_on 'Show Search Details' + expect(page.find('.js-tableBody')).to have_text('Testing Ticket 2') + expect(page.find('.js-tableBody')).to have_no_text('Testing Ticket 1') + expect(current_url).to include('Testing%20Ticket%202') + end + end + + context 'when search is changed via url' do + before do + visit '#search/"Testing Ticket 1"' + end + + it 'does switch search results properly' do + expect(page.find('.js-tableBody')).to have_text('Testing Ticket 1') + expect(page.find('.js-tableBody')).to have_no_text('Testing Ticket 2') + expect(current_url).to include('Testing%20Ticket%201') + + # switch by url + visit '#search/"Testing Ticket 2"' + expect(page.find('.js-tableBody')).to have_text('Testing Ticket 2') + expect(page.find('.js-tableBody')).to have_no_text('Testing Ticket 1') + expect(current_url).to include('Testing%20Ticket%202') + end + end + end end diff --git a/spec/system/settings/security_spec.rb b/spec/system/settings/security_spec.rb index 4449ef959..dfa2c4e49 100644 --- a/spec/system/settings/security_spec.rb +++ b/spec/system/settings/security_spec.rb @@ -68,12 +68,32 @@ RSpec.describe 'Manage > Settings > Security', type: :system do end end + shared_examples 'Display callback urls for third-party applications #3622' do + def callback_url + page.evaluate_script("$('[data-name=#{app_setting}]').closest('.page-header').parent().find('[data-attribute-name=callback_url] input').val()") + end + + context 'Display callback urls for third-party applications #3622', authenticated_as: true do + before do + visit '/#settings/security' + within :active_content do + click 'a[href="#third_party_auth"]' + end + end + + it 'does have a filled callback url' do + expect(callback_url).to be_present + end + end + end + describe 'Authentication via Facebook' do let(:app_name) { 'Facebook' } let(:app_setting) { 'auth_facebook' } include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via Github' do @@ -82,6 +102,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via GitLab' do @@ -90,6 +111,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via Google' do @@ -98,6 +120,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via LinkedIn' do @@ -106,6 +129,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via Office 365' do @@ -114,6 +138,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via SAML' do @@ -122,6 +147,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via SSO' do @@ -138,6 +164,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end describe 'Authentication via Weibo' do @@ -146,6 +173,7 @@ RSpec.describe 'Manage > Settings > Security', type: :system do include_examples 'for third-party applications button in login page' include_examples 'for third-party applications settings' + include_examples 'Display callback urls for third-party applications #3622' end end end diff --git a/spec/system/system/integration/smime_spec.rb b/spec/system/system/integration/smime_spec.rb index 0d1ad99ce..13c5dab8c 100644 --- a/spec/system/system/integration/smime_spec.rb +++ b/spec/system/system/integration/smime_spec.rb @@ -23,44 +23,65 @@ RSpec.describe 'Manage > Integration > S/MIME', type: :system do click 'label[for=setting-switch]' end - it 'enabling and adding of public and private key' do + context 'when doing basic tests' do + it 'enabling and adding of public and private key' do - # add cert - click '.js-addCertificate' - fill_in 'Paste Certificate', with: certificate - click '.js-submit' + # add cert + click '.js-addCertificate' + fill_in 'Paste Certificate', with: certificate + click '.js-submit' - # add private key - click '.js-addPrivateKey' - fill_in 'Paste Private Key', with: private_key - fill_in 'Enter Private Key secret', with: private_key_secret - click '.js-submit' + # add private key + click '.js-addPrivateKey' + fill_in 'Paste Private Key', with: private_key + fill_in 'Enter Private Key secret', with: private_key_secret + click '.js-submit' - # wait for ajax - expect(page).to have_css('td', text: 'Including private key') + # check result + expect(Setting.get('smime_integration')).to be true + expect(SMIMECertificate.last.fingerprint).to be_present + expect(SMIMECertificate.last.raw).to be_present + expect(SMIMECertificate.last.private_key).to be_present + end - # check result - expect(Setting.get('smime_integration')).to be true - expect(SMIMECertificate.last.fingerprint).to be_present - expect(SMIMECertificate.last.raw).to be_present - expect(SMIMECertificate.last.private_key).to be_present + it 'adding of multiple certificates at once' do + multiple_certificates = [ + File.read(Rails.root.join('spec/fixtures/smime/ChainCA.crt')), + File.read(Rails.root.join('spec/fixtures/smime/IntermediateCA.crt')), + File.read(Rails.root.join('spec/fixtures/smime/RootCA.crt')), + ].join + + # add cert + click '.js-addCertificate' + fill_in 'Paste Certificate', with: multiple_certificates + click '.js-submit' + + # wait for ajax + expect(page).to have_text('ChainCA') + expect(page).to have_text('IntermediateCA') + expect(page).to have_text('RootCA') + end end - it 'adding of multiple certificates at once' do - multiple_certificates = [ - File.read(Rails.root.join('spec/fixtures/smime/ChainCA.crt')), - File.read(Rails.root.join('spec/fixtures/smime/IntermediateCA.crt')), - File.read(Rails.root.join('spec/fixtures/smime/RootCA.crt')), - ].join + context 'Adding private keys allows adding certificates #3727' do + let!(:private_key) do + File.read(Rails.root.join('spec/fixtures/smime/issue_3727.key')) + end - # add cert - click '.js-addCertificate' - fill_in 'Paste Certificate', with: multiple_certificates - click '.js-submit' + it 'does add public and private key in one file' do - # wait for ajax - expect(page).to have_text('ChainCA') - expect(page).to have_text('IntermediateCA') - expect(page).to have_text('RootCA') + # add private key + click '.js-addPrivateKey' + fill_in 'Paste Private Key', with: private_key + click '.js-submit' + + # check result + expect(Setting.get('smime_integration')).to be true + expect(SMIMECertificate.last.fingerprint).to eq('db49277070afcfc657bd71d04be4dd7e28f1685a') + expect(SMIMECertificate.last.raw).to include('CERTIFICATE') + expect(SMIMECertificate.last.raw).not_to include('PRIVATE') + expect(SMIMECertificate.last.private_key).to include('PRIVATE') + expect(SMIMECertificate.last.private_key).not_to include('CERTIFICATE') + end end end diff --git a/spec/system/system/object_manager_spec.rb b/spec/system/system/object_manager_spec.rb index 3fa859bdc..9ce519ca7 100644 --- a/spec/system/system/object_manager_spec.rb +++ b/spec/system/system/object_manager_spec.rb @@ -29,6 +29,19 @@ RSpec.describe 'System > Objects', type: :system do end end + context 'when creating a new attribute' do + before do + visit '/#system/object_manager' + find('.js-new').click + end + + it 'has the position feild with no default value' do + within '.modal' do + expect(page).to have_field('position', with: '') + end + end + end + context 'when creating but then discarding fields again' do before do visit '/#system/object_manager' @@ -489,4 +502,30 @@ RSpec.describe 'System > Objects', type: :system do end end end + + context 'when creating with no diff' do + before do + visit '/#system/object_manager' + page.find('.js-new').click + + in_modal disappears: false do + fill_in 'Name', with: 'nodiff' + fill_in 'Display', with: 'NoDiff' + end + end + + it 'date attribute' do + page.find('select[name=data_type]').select('Date') + fill_in 'Default time Diff (hours)', with: '' + + expect { page.find('.js-submit').click }.to change(ObjectManager::Attribute, :count).by(1) + end + + it 'datetime attribute' do + page.find('select[name=data_type]').select('Datetime') + fill_in 'Default time Diff (minutes)', with: '' + + expect { page.find('.js-submit').click }.to change(ObjectManager::Attribute, :count).by(1) + end + end end diff --git a/spec/system/ticket/create_spec.rb b/spec/system/ticket/create_spec.rb index 021661059..30c94dcd8 100644 --- a/spec/system/ticket/create_spec.rb +++ b/spec/system/ticket/create_spec.rb @@ -563,6 +563,25 @@ RSpec.describe 'Ticket Create', type: :system do end end + context 'when state options have a special translation', authenticated_as: :authenticate do + let(:admin_de) { create(:admin, preferences: { locale: 'de-de' }) } + + context 'when translated state option has a single quote' do + def authenticate + open_tranlation = Translation.where(locale: 'de-de', source: 'open') + open_tranlation.update(target: "off'en") + + admin_de + end + + it 'shows the translated state options correctly' do + visit 'ticket/create' + + expect(page).to have_select('state_id', with_options: ["off'en"]) + end + end + end + describe 'It should be possible to show attributes which are configured shown false #3726', authenticated_as: :authenticate, db_strategy: :reset do let(:field_name) { SecureRandom.uuid } let(:field) do @@ -665,4 +684,177 @@ RSpec.describe 'Ticket Create', type: :system do expect(Ticket.last.pending_time).to be nil end end + + describe 'When looking for customers, it is no longer possible to change into organizations #3815' do + before do + visit 'ticket/create' + + # modal reaper ;) + sleep 3 + end + + context 'when less than 10 customers' do + let(:organization) { Organization.first } + + it 'has no show more option' do + find('[name=customer_id_completion]').fill_in with: 'zam' + expect(page).to have_selector("li.js-organization[data-organization-id='#{organization.id}']") + page.find("li.js-organization[data-organization-id='#{organization.id}']").click + expect(page).to have_selector("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li.js-showMoreMembers.hidden", visible: :all) + end + end + + context 'when more than 10 customers', authenticated_as: :authenticate do + def authenticate + customers + true + end + + let(:organization) { create(:organization, name: 'Zammed') } + let(:customers) do + create_list(:customer, 50, organization: organization) + end + + it 'does paginate through organization' do + find('[name=customer_id_completion]').fill_in with: 'zam' + expect(page).to have_selector("li.js-organization[data-organization-id='#{organization.id}']") + page.find("li.js-organization[data-organization-id='#{organization.id}']").click + wait(5).until { page.all("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li", visible: :all).count == 12 } # 10 users + back + show more button + + expect(page).to have_selector("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li.js-showMoreMembers[organization-member-limit='10']") + scroll_into_view('li.js-showMoreMembers') + page.find("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li.js-showMoreMembers").click + wait(5).until { page.all("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li", visible: :all).count == 27 } # 25 users + back + show more button + + expect(page).to have_selector("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li.js-showMoreMembers[organization-member-limit='25']") + scroll_into_view('li.js-showMoreMembers') + page.find("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li.js-showMoreMembers").click + wait(5).until { page.all("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li", visible: :all).count == 52 } # 50 users + back + show more button + + scroll_into_view('li.js-showMoreMembers') + expect(page).to have_selector("ul.recipientList-organizationMembers[organization-id='#{organization.id}'] li.js-showMoreMembers.hidden", visible: :all, wait: 20) + end + end + end + + describe 'Ticket create screen will loose attachments by time #3827' do + before do + visit 'ticket/create' + end + + it 'does not loose attachments on rerender of the ui' do + # upload two files + page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box')) + await_empty_ajax_queue + page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail002.box')) + await_empty_ajax_queue + wait(5).until { page.all('div.attachment-delete.js-delete', visible: :all).count == 2 } + expect(page).to have_text('mail001.box') + expect(page).to have_text('mail002.box') + + # remove last file + begin + page.evaluate_script("$('div.attachment-delete.js-delete:last').click()") # not interactable + rescue # Lint/SuppressedException + # because its not interactable it also + # returns this weird exception for the jquery + # even tho it worked fine + end + await_empty_ajax_queue + wait(5).until { page.all('div.attachment-delete.js-delete', visible: :all).count == 1 } + expect(page).to have_text('mail001.box') + expect(page).to have_no_text('mail002.box') + + # simulate rerender b + page.evaluate_script("App.Event.trigger('ui:rerender')") + expect(page).to have_text('mail001.box') + expect(page).to have_no_text('mail002.box') + end + end + + describe 'Invalid group and owner list for tickets created via customer profile #3835' do + let(:invalid_ticket) { create(:ticket) } + + before do + visit "#ticket/create/id/#{invalid_ticket.id}/customer/#{User.find_by(firstname: 'Nicole').id}" + end + + it 'does show an empty list of owners' do + wait(5).until { page.all('select[name=owner_id] option').count == 1 } + expect(page.all('select[name=owner_id] option').count).to eq(1) + end + end + + # https://github.com/zammad/zammad/issues/3825 + describe 'CC token field' do + before do + visit 'ticket/create' + + find('[data-type=email-out]').click + end + + it 'can be cleared by cutting out text' do + add_email 'asd@example.com' + add_email 'def@example.com' + + find('.token', text: 'def@example.com').double_click + + meta_key = Gem::Platform.local.os == 'darwin' ? :command : :control + send_keys([meta_key, 'x']) + + find('.token').click # trigger blur + + expect(find('[name="cc"]', visible: :all).value).to eq 'asd@example.com' + end + + def add_email(input) + fill_in 'Cc', with: input + send_keys(:enter) # trigger blur + find '.token', text: input # wait for email to tokenize + end + end + + describe 'No signature on new ticket if email is default message type #3844', authenticated_as: :authenticate do + def authenticate + Setting.set('ui_ticket_create_default_type', 'email-out') + Group.where.not(name: 'Users').each { |g| g.update(active: false) } + true + end + + before do + visit 'ticket/create' + end + + it 'does render the create screen with an initial core workflow state to set signatures and other defaults properly' do + expect(page.find('.richtext-content')).to have_text('Support') + end + end + + describe 'Zammad 5 mail template double signature #3816', authenticated_as: :authenticate do + let(:agent_template) { create(:agent) } + let!(:template) do + create( + :template, + :dummy_data, + group: Group.first, owner: agent_template, + body: 'Content dummy.

                          Test Other Agent

                          --
                          Super Support - Waterford Business Park
                          5201 Blue Lagoon Drive - 8th Floor & 9th Floor - Miami, 33126 USA
                          Email: hot@example.com - Web: http://www.example.com/
                          --
                          ' + ) + end + + def authenticate + Group.first.update(signature: Signature.first) + true + end + + before do + visit 'ticket/create' + find('[data-type=email-out]').click + end + + it 'does not show double signature on template usage' do + select Group.first.name, from: 'group_id' + use_template(template) + expect(page).to have_no_text('Test Other Agent') + end + end end diff --git a/spec/system/ticket/update/email_reply_spec.rb b/spec/system/ticket/update/email_reply_spec.rb index c5133ee10..8d64e111f 100644 --- a/spec/system/ticket/update/email_reply_spec.rb +++ b/spec/system/ticket/update/email_reply_spec.rb @@ -17,7 +17,7 @@ RSpec.describe 'Ticket > Update > Email Reply', current_user_id: -> { current_us it 'shows error dialog when updated value is an invalid email' do within(:active_content) do - click_reply + click_email_reply find('.token').double_click find('.js-to', visible: false).sibling('.token-input').set('test') @@ -30,7 +30,7 @@ RSpec.describe 'Ticket > Update > Email Reply', current_user_id: -> { current_us it 'updates article when updated value is a valid email' do within(:active_content) do - click_reply + click_email_reply find('.token').double_click find('.js-to', visible: false).sibling('.token-input').set('user@test.com') @@ -43,7 +43,20 @@ RSpec.describe 'Ticket > Update > Email Reply', current_user_id: -> { current_us end - def click_reply + context 'when a new recipient is added in email reply' do + it 'shows both name and email in token' do + click_email_reply + + find('.js-to', visible: false).sibling('.token-input').set(customer.email) + find('ul.ui-autocomplete li:first-child', visible: false).click + + within all('.token-label')[1] do + expect(page).to have_text("#{customer.firstname} #{customer.lastname} <#{customer.email}>") + end + end + end + + def click_email_reply click '.js-ArticleAction[data-type=emailReply]' end diff --git a/spec/system/ticket/update/full_quote_header_spec.rb b/spec/system/ticket/update/full_quote_header_spec.rb index b3fd84b6d..a230702fa 100644 --- a/spec/system/ticket/update/full_quote_header_spec.rb +++ b/spec/system/ticket/update/full_quote_header_spec.rb @@ -21,7 +21,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr context 'when "ui_ticket_zoom_article_email_full_quote_header" is enabled' do let(:full_quote_header_setting) { true } - it 'includes OP when forwarding' do + it 'includes sender when forwarding' do within(:active_content) do click_forward @@ -31,7 +31,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr end end - it 'includes OP when replying' do + it 'includes sender when replying' do within(:active_content) do highlight_and_click_reply @@ -41,7 +41,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr end end - it 'includes OP when article visibility toggled' do + it 'includes sender when article visibility toggled' do within(:active_content) do set_internal highlight_and_click_reply @@ -55,7 +55,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr context 'when customer is agent' do let(:customer) { create(:agent) } - it 'includes OP without email when forwarding' do + it 'includes sender without email when forwarding' do within(:active_content) do click_forward @@ -66,13 +66,32 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr end end + # https://github.com/zammad/zammad/issues/3824 + context 'when TO contains multiple senders and one of them is a known Zammad user' do + let(:customer) { create(:customer) } + let(:to_1) { "#{customer.fullname} <#{customer.email}>" } + let(:to_2) { 'Example Two ' } + + let(:ticket_article) { create(:ticket_article, ticket: ticket, to: [to_1, to_2].join(', ')) } + + it 'includes all TO email address' do + within(:active_content) do + click_forward + + within(:richtext) do + expect(page).to have_text(to_1).and(have_text(to_2)) + end + end + end + end + context 'ticket is created by agent on behalf of customer' do let(:agent) { create(:agent) } let(:current_user) { agent } let(:ticket) { create(:ticket, group: group, title: 'Created by agent on behalf of a customer', customer: customer) } let(:ticket_article) { create(:ticket_article, ticket: ticket, from: 'Created by agent on behalf of a customer', origin_by_id: customer.id) } - it 'includes OP without email when replying' do + it 'includes sender without email when replying' do within(:active_content) do highlight_and_click_reply @@ -82,12 +101,34 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr end end end + + # https://github.com/zammad/zammad/issues/3855 + context 'when ticket article has no recipient' do + shared_examples 'when recipient is set to' do |recipient:, recipient_human:| + context "when recipient is set to #{recipient_human}" do + let(:ticket_article) { create(:ticket_article, :inbound_web, ticket: ticket, to: recipient) } + + it 'allows to forward without original recipient present' do + within(:active_content) do + click_forward + + within(:richtext) do + expect(page).to contain_full_quote(ticket_article).formatted_for(:forward) + end + end + end + end + end + + include_examples 'when recipient is set to', recipient: '', recipient_human: 'empty string' + include_examples 'when recipient is set to', recipient: nil, recipient_human: 'nil' + end end context 'when "ui_ticket_zoom_article_email_full_quote_header" is disabled' do let(:full_quote_header_setting) { false } - it 'does not include OP when forwarding' do + it 'does not include sender when forwarding' do within(:active_content) do click_forward @@ -97,7 +138,7 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr end end - it 'does not include OP when replying' do + it 'does not include sender when replying' do within(:active_content) do highlight_and_click_reply @@ -178,6 +219,31 @@ RSpec.describe 'Ticket > Update > Full Quote Header', current_user_id: -> { curr end end + context 'when full quote header setting is enabled' do + let(:full_quote_header_setting) { true } + + it 'can breakout with enter from quote block' do + within(:active_content) do + highlight_and_click_reply + + within(:richtext) do + blockquote_empty_line = first('blockquote br:nth-child(2)', visible: :all) + page.driver.browser.action.move_to_location(blockquote_empty_line.native.location.x, blockquote_empty_line.native.location.y).click.perform + end + + # Special handling for firefox, because the cursor is at the wrong location after the move to with click. + if Capybara.current_driver == :zammad_firefox + find(:richtext).send_keys(:down) + end + + find(:richtext).send_keys(:enter) + + within(:richtext) do + expect(page).to have_css('blockquote', count: 2) + end + end + end + end end def click_forward diff --git a/spec/system/ticket/update_spec.rb b/spec/system/ticket/update_spec.rb index 09cb8cfe1..3f56e6420 100644 --- a/spec/system/ticket/update_spec.rb +++ b/spec/system/ticket/update_spec.rb @@ -322,10 +322,10 @@ RSpec.describe 'Ticket Update', type: :system do end context 'when group will be changed' do - let(:user) { create(:user) } + let(:user) { User.find_by(email: 'agent1@example.com') } let(:ticket) { create(:ticket, group: group, owner: user) } - it 'check that owner is resetet after group change' do + it 'check that owner resets after group change' do visit "#ticket/zoom/#{ticket.id}" expect(page).to have_field('owner_id', with: user.id) diff --git a/spec/system/ticket/view_spec.rb b/spec/system/ticket/view_spec.rb index 67dd7631c..7a61cd2d4 100644 --- a/spec/system/ticket/view_spec.rb +++ b/spec/system/ticket/view_spec.rb @@ -363,7 +363,7 @@ RSpec.describe 'Ticket views', type: :system do true end - it 'does show the date groups sorted' do + it 'does show the values grouped and sorted by date key value (yyy-mm-dd) instead of display value' do visit 'ticket/view/all_unassigned' text = page.find('.js-tableBody').text(:all) @@ -372,5 +372,42 @@ RSpec.describe 'Ticket views', type: :system do expect(text.index('01/19/2019') < text.index('08/18/2021')).to eq(true) end end + + context 'when sorted by custom object select', authenticated_as: :authenticate, db_strategy: :reset do + let(:ticket1) { create(:ticket, group: Group.find_by(name: 'Users'), cselect: 'a') } + let(:ticket2) { create(:ticket, group: Group.find_by(name: 'Users'), cselect: 'b') } + let(:ticket3) { create(:ticket, group: Group.find_by(name: 'Users'), cselect: 'c') } + + def authenticate + create :object_manager_attribute_select, name: 'cselect', data_option: { + 'default' => '', + 'options' => { + 'a' => 'Zzz a', + 'b' => 'Yyy b', + 'c' => 'Xxx c', + }, + 'relation' => '', + 'nulloption' => true, + 'multiple' => false, + 'null' => true, + 'translate' => true, + 'maxlength' => 255 + } + ObjectManager::Attribute.migration_execute + ticket1 + ticket2 + ticket3 + Overview.find_by(link: 'all_unassigned').update(group_by: 'cselect') + true + end + + it 'does show the values grouped and sorted by display value instead of key value' do + visit 'ticket/view/all_unassigned' + text = page.find('.js-tableBody').text(:all) + + expect(text.index('Xxx c') < text.index('Yyy b')).to eq(true) + expect(text.index('Yyy b') < text.index('Zzz a')).to eq(true) + end + end end end diff --git a/spec/system/ticket/zoom_spec.rb b/spec/system/ticket/zoom_spec.rb index 07b57b31d..f7e988c95 100644 --- a/spec/system/ticket/zoom_spec.rb +++ b/spec/system/ticket/zoom_spec.rb @@ -2011,13 +2011,13 @@ RSpec.describe 'Ticket zoom', type: :system do end def expect_upload_and_text - expect(page).to have_text('mail001.box') - expect(page).to have_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test") + expect(page.find('.article-new')).to have_text('mail001.box') + expect(page.find('.article-new')).to have_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test") end def expect_no_upload_and_text - expect(page).to have_no_text('mail001.box') - expect(page).to have_no_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test") + expect(page.find('.article-new')).to have_no_text('mail001.box') + expect(page.find('.article-new')).to have_no_text("Hello\nThis\nis\nimportant!\nyo\nhoho\ntest test test test") end it 'does show up the attachments after a reload of the page' do @@ -2044,6 +2044,55 @@ RSpec.describe 'Ticket zoom', type: :system do refresh expect_no_upload_and_text end + + context 'when rerendering (#3831)' do + def rerender + page.evaluate_script("App.Event.trigger('ui:rerender')") + end + + it 'does loose attachments after rerender' do + upload_and_set_text + expect_upload_and_text + rerender + expect_upload_and_text + end + + it 'does not readd the attachments after reset' do + upload_and_set_text + expect_upload_and_text + + page.find('.js-reset').click + wait_for_upload_blank + expect_no_upload_and_text + rerender + expect_no_upload_and_text + end + + it 'does not readd the attachments after submit' do + upload_and_set_text + expect_upload_and_text + + page.find('.js-submit').click + wait_for_upload_blank + expect_no_upload_and_text + rerender + expect_no_upload_and_text + end + + it 'does not show the ticket as changed after the upload removal' do + page.find('input#fileUpload_1', visible: :all).set(Rails.root.join('test/data/mail/mail001.box')) + expect(page.find('.article-new')).to have_text('mail001.box') + wait_for_upload_present + begin + page.evaluate_script("$('div.attachment-delete.js-delete:last').click()") # not interactable + rescue # Lint/SuppressedException + # because its not interactable it also + # returns this weird exception for the jquery + # even tho it worked fine + end + expect(page).to have_no_selector('.js-reset') + end + end end describe 'Unable to close tickets in certran cases if core workflow is used #3710', authenticated_as: :authenticate, db_strategy: :reset do @@ -2149,6 +2198,13 @@ RSpec.describe 'Ticket zoom', type: :system do expect(page.find("select[name='priority_id']").value).to eq(high_prio.id.to_s) end + it 'does show up the new group (different case because it will also trigger a full rerender because of potential permission changes)' do + group = Group.find_by(name: 'some group1') + ticket.update(group: group) + wait(10, interval: 0.5).until { page.find("select[name='group_id']").value == group.id.to_s } + expect(page.find("select[name='group_id']").value).to eq(group.id.to_s) + end + it 'does show up the new state and pending time' do pending_state = Ticket::State.find_by(name: 'pending reminder') ticket.update(state: pending_state, pending_time: 1.day.from_now) @@ -2228,4 +2284,66 @@ RSpec.describe 'Ticket zoom', type: :system do expect(page).to have_selector('form.article-add.is-open') end end + + context 'Owner should get cleared if not listed in changed group #3818', authenticated_as: :authenticate do + let(:group1) { create(:group) } + let(:group2) { create(:group) } + let(:agent1) { create(:agent) } + let(:agent2) { create(:agent) } + let(:ticket) { create(:ticket, group: group1, owner: agent1) } + + def authenticate + agent1.group_names_access_map = { + group1.name => 'full', + group2.name => %w[read change overview] + } + agent2.group_names_access_map = { + group1.name => 'full', + group2.name => 'full', + } + agent1 + end + + before do + visit "#ticket/zoom/#{ticket.id}" + end + + it 'does clear agent1 on select of group 2' do + select group2.name, from: 'Group' + wait(5).until { page.find('select[name=owner_id]').value != agent1.id.to_s } + expect(page.find('select[name=owner_id]').value).to eq('') + expect(page.all('select[name=owner_id] option').map(&:value)).not_to include(agent1.id.to_s) + expect(page.all('select[name=owner_id] option').map(&:value)).to include(agent2.id.to_s) + end + end + + describe 'Changing ticket status from "new" to any other status always results in uncommited status "closed" #3880', authenticated_as: :authenticate do + let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users')) } + let(:workflow) do + create(:core_workflow, + object: 'Ticket', + condition_selected: { + 'ticket.priority_id': { + operator: 'is', + value: [ Ticket::Priority.find_by(name: '3 high').id.to_s ], + }, + }, + perform: { 'ticket.state_id' => { operator: 'remove_option', remove_option: [ Ticket::State.find_by(name: 'pending reminder').id.to_s ] } }) + end + + def authenticate + workflow + true + end + + before do + visit "#ticket/zoom/#{ticket.id}" + end + + it 'does switch back to the saved value in the ticket instead of the first value of the dropdown' do + page.select 'pending reminder', from: 'state_id' + page.select '3 high', from: 'priority_id' + expect(page).to have_select('state_id', selected: 'new') + end + end end diff --git a/test/data/mail/mail102.box b/test/data/mail/mail102.box index 2b1bb2097..cf02983fc 100644 --- a/test/data/mail/mail102.box +++ b/test/data/mail/mail102.box @@ -29,6 +29,9 @@ Content-Type: text/plain; Text +new line + +Test 123 --Apple-Mail=_57B3D7B4-58AD-4E7A-9A4A-5053521E2862 Content-Disposition: inline; @@ -6646,5 +6649,8 @@ Content-Type: text/plain; Text +Text line + +end --Apple-Mail=_57B3D7B4-58AD-4E7A-9A4A-5053521E2862-- diff --git a/test/data/mail/mail102.yml b/test/data/mail/mail102.yml index 4781b5e0b..d34917964 100644 --- a/test/data/mail/mail102.yml +++ b/test/data/mail/mail102.yml @@ -4,21 +4,8 @@ from_email: top@secret.tld from_display_name: Mr. Top Secret to: zammad@XXXXXXXXX.de subject: Text - Bild - Text - PDF - Text - Bild - Text -body: |+ - Text - - - - Text - - - - Text - - - - Text - +body: Text

                          new line

                          Test 123


                          Text



                          Text



                          Text

                          Text line

                          end
                          content_type: text/html attachments: - !ruby/hash:ActiveSupport::HashWithIndifferentAccess diff --git a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_credentials.yml b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_credentials.yml index 1a4ba786e..9576628a0 100644 --- a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_credentials.yml +++ b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_credentials.yml @@ -21,7 +21,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 08 Jul 2021 14:44:52 GMT + - Tue, 04 Jan 2022 12:44:06 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -31,7 +31,7 @@ http_interactions: Status: - 401 Unauthorized X-Request-Id: - - 0ce1a277-88bd-4d52-9202-e7c40b92cb38 + - 6aa908c1-0f2e-43b8-a240-6b0e6389865e X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -51,20 +51,20 @@ http_interactions: X-Ratelimit-Total: - '40' X-Ratelimit-Remaining: - - '39' + - '38' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '31' + - '38' X-Trace-Id: - - 00-51ed9c1c0a83b39abc54bc0c33cc642b-dc8bddd14d14bc03-00 + - 00-c86e324be3fbb8d36ea12b763341ab25-1af8213b43da7a01-00 Server: - fwe body: encoding: UTF-8 string: '{"code":"invalid_credentials","message":"You have to be logged in to perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:44:52 GMT + recorded_at: Tue, 04 Jan 2022 12:44:06 GMT - request: method: get uri: https://.freshdesk.com/api/v2/agents/me @@ -90,7 +90,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 08 Jul 2021 14:44:53 GMT + - Tue, 04 Jan 2022 12:44:08 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -100,7 +100,7 @@ http_interactions: Status: - 401 Unauthorized X-Request-Id: - - 5719108c-486f-40f0-9fdf-7caba98042f7 + - 623010a9-972b-4f0f-96dd-274ca3ca1824 X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -120,20 +120,20 @@ http_interactions: X-Ratelimit-Total: - '100' X-Ratelimit-Remaining: - - '98' + - '95' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '39' + - '38' X-Trace-Id: - - 00-5c35e85dfee04cac7f75d7ab33d64969-f8f94253325a913e-00 + - 00-1cbd052b2b9c630d1bb6a6fa4c9a5a4b-46afe1e1c9f07eb4-00 Server: - fwe body: encoding: UTF-8 string: '{"code":"invalid_credentials","message":"You have to be logged in to perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:44:53 GMT + recorded_at: Tue, 04 Jan 2022 12:44:08 GMT - request: method: get uri: https://.freshdesk.com/api/v2/agents/me @@ -159,7 +159,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 08 Jul 2021 14:44:53 GMT + - Tue, 04 Jan 2022 12:44:08 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -169,7 +169,7 @@ http_interactions: Status: - 401 Unauthorized X-Request-Id: - - ca94e08c-04a5-44ad-837e-7dd0cb693fd9 + - 8db4db19-a022-9a66-bb34-f53f9fdefadd X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -189,18 +189,18 @@ http_interactions: X-Ratelimit-Total: - '100' X-Ratelimit-Remaining: - - '97' + - '94' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '75' + - '41' X-Trace-Id: - - 00-3ed5efb5331d9b0e6cfd35a96da0e7ca-7a1106d2c14516ba-00 + - 00-ebf6f2a91d6b0a6fad1f8c2cafd57b91-dedbf40ba0772163-01 Server: - fwe body: encoding: UTF-8 string: '{"code":"invalid_credentials","message":"You have to be logged in to perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:44:53 GMT + recorded_at: Tue, 04 Jan 2022 12:44:08 GMT recorded_with: VCR 6.0.0 diff --git a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_hostname.yml b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_hostname.yml index 02472e4be..9065c9daa 100644 --- a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_hostname.yml +++ b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_invalid_hostname.yml @@ -21,7 +21,7 @@ http_interactions: message: Not Found headers: Date: - - Thu, 08 Jul 2021 14:44:47 GMT + - Tue, 04 Jan 2022 12:44:01 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -31,7 +31,7 @@ http_interactions: Status: - 404 Not Found X-Request-Id: - - edf9bb99-1c3c-48df-875e-455cbd963c52 + - b4148d62-dba8-4793-863d-1b105508d70f X-Rack-Cache: - miss Cache-Control: @@ -45,13 +45,13 @@ http_interactions: X-Fw-Ratelimiting-Managed: - 'false' X-Envoy-Upstream-Service-Time: - - '29' + - '35' X-Trace-Id: - - 00-e50f39ffa8d89a7ab2f260463b10436e-4d93e6d797831b87-00 + - 00-5dc27c3d059e008ef1749ca95a2ccf82-c7226e440cca5cf6-00 Server: - fwe body: encoding: ASCII-8BIT string: " " - recorded_at: Thu, 08 Jul 2021 14:44:47 GMT + recorded_at: Tue, 04 Jan 2022 12:44:01 GMT recorded_with: VCR 6.0.0 diff --git a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_shows_start_button.yml b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_shows_start_button.yml index 63b688025..6cd55c190 100644 --- a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_shows_start_button.yml +++ b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_shows_start_button.yml @@ -21,7 +21,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 08 Jul 2021 14:44:59 GMT + - Tue, 04 Jan 2022 12:43:13 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -31,7 +31,7 @@ http_interactions: Status: - 401 Unauthorized X-Request-Id: - - 23514661-1b1e-4794-af85-4258cd13df06 + - 5745f576-faca-4cf5-84dc-66da88902de7 X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -51,20 +51,20 @@ http_interactions: X-Ratelimit-Total: - '40' X-Ratelimit-Remaining: - - '37' + - '39' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '36' + - '48' X-Trace-Id: - - 00-1369aa8348250d3161f7be598ec434f4-2966dbe53b8eae9f-00 + - 00-42b68f35b1e7b55b914e542cd24f1194-25d22a931984720d-00 Server: - fwe body: encoding: UTF-8 string: '{"code":"invalid_credentials","message":"You have to be logged in to perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:44:59 GMT + recorded_at: Tue, 04 Jan 2022 12:43:13 GMT - request: method: get uri: https://.freshdesk.com/api/v2/agents/me @@ -90,7 +90,7 @@ http_interactions: message: OK headers: Date: - - Thu, 08 Jul 2021 14:45:01 GMT + - Tue, 04 Jan 2022 12:43:15 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -102,7 +102,7 @@ http_interactions: Pragma: - no-cache X-Request-Id: - - d5ab60cd-84d3-4816-8a52-01410cb3c82f + - 993f3356-2352-4fb3-9788-25732c019fe9 X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -118,7 +118,7 @@ http_interactions: Expires: - Wed, 13 Oct 2010 00:00:00 UTC Set-Cookie: - - _helpkit_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJTQ0MjMyNGNkZWQ3YjhjZDE5ZTU2YWI5ZWFiZmYwYWRmBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMVFnWXQveElsdWNnY2RaR3NiOXAwRWVjbU1EZk9ISWFPYlJpYXNCaEgyQUk9BjsARg%3D%3D--7c926b47ede8867f6c888c5df8a516fea8b1be9a; + - _helpkit_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJTUyNmE2MTg5OGRhNjY4NzUwMGU5Mjg1MzY4YzQyMzJkBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMU9RWkc5bjlBQlh3cmc2V1MvSll6bnRvREFncDVuaWxGdTVzVnBDaUlvdXc9BjsARg%3D%3D--af05d1409834c6632dd49fc0b6f3a5a704655a1f; path=/; HttpOnly; secure - _x_w=5_2; path=/; HttpOnly; secure X-Fw-Ratelimiting-Managed: @@ -126,18 +126,92 @@ http_interactions: X-Ratelimit-Total: - '100' X-Ratelimit-Remaining: - - '92' + - '98' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '51' + - '78' X-Trace-Id: - - 00-1907904008abae8ffb674d4d6e17d131-f421d789c393542e-00 + - 00-76b5ddcacdcb6167517e3ce8ab8730f1-e607ea520c3e4b2f-00 Server: - fwe body: encoding: ASCII-8BIT string: '{"available":false,"occasional":false,"id":80014400475,"ticket_scope":1,"signature":null,"group_ids":[],"role_ids":[80000198826],"skill_ids":[],"available_since":null,"contact":{"active":true,"email":"info@.org","job_title":null,"language":"en","mobile":null,"name":"Thorsten - Eckel","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:23:58Z","updated_at":"2021-04-09T13:31:00Z","last_login_at":"2021-06-07T05:38:22Z"},"created_at":"2021-04-09T13:23:58Z","updated_at":"2021-07-08T07:08:47Z","type":"support_agent"}' - recorded_at: Thu, 08 Jul 2021 14:45:01 GMT + Eckel","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:23:58Z","updated_at":"2021-04-09T13:31:00Z","last_login_at":"2021-10-07T17:33:11Z"},"created_at":"2021-04-09T13:23:58Z","updated_at":"2022-01-03T18:35:31Z","type":"support_agent"}' + recorded_at: Tue, 04 Jan 2022 12:43:15 GMT +- request: + method: get + uri: https://.freshdesk.com/api/v2/agents + body: + encoding: US-ASCII + string: '' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - ".freshdesk.com" + Authorization: + - Basic + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Jan 2022 12:43:15 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Status: + - 200 OK + Pragma: + - no-cache + X-Request-Id: + - 5826ff4b-05d3-91c4-b4ab-1d6bf8508335 + X-Freshdesk-Api-Version: + - latest=v2; requested=v2 + X-Rack-Cache: + - miss + Cache-Control: + - must-revalidate, no-cache, no-store, private, max-age=0 + X-Xss-Protection: + - 1; mode=block + X-Ua-Compatible: + - IE=Edge,chrome=1 + X-Content-Type-Options: + - nosniff + Expires: + - Wed, 13 Oct 2010 00:00:00 UTC + Set-Cookie: + - _x_w=5_2; path=/; HttpOnly; secure + X-Fw-Ratelimiting-Managed: + - 'true' + X-Ratelimit-Total: + - '100' + X-Ratelimit-Remaining: + - '97' + X-Ratelimit-Used-Currentrequest: + - '1' + X-Envoy-Upstream-Service-Time: + - '63' + X-Trace-Id: + - 00-b8ca4c0600032eb7fcb920b2abd0013d-ab8383a7478ac77f-01 + Server: + - fwe + body: + encoding: ASCII-8BIT + string: '[{"available":false,"occasional":true,"id":80014400480,"ticket_scope":1,"created_at":"2021-04-09T13:24:01Z","updated_at":"2021-04-09T13:24:01Z","last_active_at":null,"available_since":null,"type":"support_agent","contact":{"active":false,"email":"custserv@freshdesk.com","job_title":null,"language":"en","last_login_at":null,"mobile":null,"name":"Customer + Service","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:24:01Z","updated_at":"2021-04-09T13:24:01Z"},"signature":null},{"available":false,"occasional":false,"id":80014400475,"ticket_scope":1,"created_at":"2021-04-09T13:23:58Z","updated_at":"2022-01-03T18:35:31Z","last_active_at":"2022-01-03T18:35:31Z","available_since":null,"type":"support_agent","contact":{"active":true,"email":"info@.org","job_title":null,"language":"en","last_login_at":"2021-10-07T17:33:11Z","mobile":null,"name":"Thorsten + Eckel","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:23:58Z","updated_at":"2021-04-09T13:31:00Z"},"signature":null}]' + recorded_at: Tue, 04 Jan 2022 12:43:15 GMT recorded_with: VCR 6.0.0 diff --git a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_credentials.yml b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_credentials.yml index 96a8d3cee..a17683764 100644 --- a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_credentials.yml +++ b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_credentials.yml @@ -21,7 +21,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 08 Jul 2021 14:44:55 GMT + - Tue, 04 Jan 2022 12:44:10 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -31,7 +31,7 @@ http_interactions: Status: - 401 Unauthorized X-Request-Id: - - 7902d2cc-0bee-4f7c-9422-0a803602306b + - df91e7c3-2063-4793-a226-6059dd715c90 X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -51,20 +51,20 @@ http_interactions: X-Ratelimit-Total: - '40' X-Ratelimit-Remaining: - - '38' + - '37' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '44' + - '47' X-Trace-Id: - - 00-bf646768b4fb3bbf4733ab1d89f9cc65-d5c0ee890610da12-00 + - 00-efa020d9face7343512e02716bda4cb9-c249a978507c79a2-00 Server: - fwe body: encoding: UTF-8 string: '{"code":"invalid_credentials","message":"You have to be logged in to perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:44:55 GMT + recorded_at: Tue, 04 Jan 2022 12:44:10 GMT - request: method: get uri: https://.freshdesk.com/api/v2/agents/me @@ -90,7 +90,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 08 Jul 2021 14:44:57 GMT + - Tue, 04 Jan 2022 12:44:11 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -100,7 +100,7 @@ http_interactions: Status: - 401 Unauthorized X-Request-Id: - - 8007c6d5-96f0-9608-9571-59cad9b5d205 + - a0b5cf13-ca57-4d83-83e1-0549d05bb89e X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -120,87 +120,87 @@ http_interactions: X-Ratelimit-Total: - '100' X-Ratelimit-Remaining: - - '95' + - '92' + X-Ratelimit-Used-Currentrequest: + - '1' + X-Envoy-Upstream-Service-Time: + - '40' + X-Trace-Id: + - 00-5093a663f6de7078b6f259501bcbad5c-16038b8e950af115-00 + Server: + - fwe + body: + encoding: UTF-8 + string: '{"code":"invalid_credentials","message":"You have to be logged in to + perform this action."}' + recorded_at: Tue, 04 Jan 2022 12:44:11 GMT +- request: + method: get + uri: https://.freshdesk.com/api/v2/agents/me + body: + encoding: US-ASCII + string: '' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - ".freshdesk.com" + Authorization: + - Basic MW52NGwxZFQwSzNOOlg= + response: + status: + code: 401 + message: Unauthorized + headers: + Date: + - Tue, 04 Jan 2022 12:44:12 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Status: + - 401 Unauthorized + X-Request-Id: + - 9d6cf8b0-d3f9-4156-aeb3-09083bf2a737 + X-Freshdesk-Api-Version: + - latest=v2; requested=v2 + X-Rack-Cache: + - miss + Cache-Control: + - no-cache, private + X-Xss-Protection: + - 1; mode=block + X-Ua-Compatible: + - IE=Edge,chrome=1 + X-Content-Type-Options: + - nosniff + Set-Cookie: + - _x_w=5_2; path=/; HttpOnly; secure + X-Fw-Ratelimiting-Managed: + - 'true' + X-Ratelimit-Total: + - '100' + X-Ratelimit-Remaining: + - '91' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - '36' X-Trace-Id: - - 00-3740e89fc64ccd0a3b92ed13203aeaf7-e34c3c1b71452bba-01 + - 00-ec39a5c61d07b7fdbd8100bd353311fd-66ac1c542a6efa68-00 Server: - fwe body: encoding: UTF-8 string: '{"code":"invalid_credentials","message":"You have to be logged in to perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:44:57 GMT -- request: - method: get - uri: https://.freshdesk.com/api/v2/agents/me - body: - encoding: US-ASCII - string: '' - headers: - Content-Type: - - application/json - Accept-Encoding: - - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 - Accept: - - "*/*" - User-Agent: - - Ruby - Host: - - ".freshdesk.com" - Authorization: - - Basic MW52NGwxZFQwSzNOOlg= - response: - status: - code: 401 - message: Unauthorized - headers: - Date: - - Thu, 08 Jul 2021 14:44:57 GMT - Content-Type: - - application/json; charset=utf-8 - Transfer-Encoding: - - chunked - Connection: - - keep-alive - Status: - - 401 Unauthorized - X-Request-Id: - - 0b8d8dee-e471-40db-ae98-962331e21405 - X-Freshdesk-Api-Version: - - latest=v2; requested=v2 - X-Rack-Cache: - - miss - Cache-Control: - - no-cache, private - X-Xss-Protection: - - 1; mode=block - X-Ua-Compatible: - - IE=Edge,chrome=1 - X-Content-Type-Options: - - nosniff - Set-Cookie: - - _x_w=5_2; path=/; HttpOnly; secure - X-Fw-Ratelimiting-Managed: - - 'true' - X-Ratelimit-Total: - - '100' - X-Ratelimit-Remaining: - - '94' - X-Ratelimit-Used-Currentrequest: - - '1' - X-Envoy-Upstream-Service-Time: - - '78' - X-Trace-Id: - - 00-02cfc4ead37ab53768d74f2055525b82-acaa880886cf4572-00 - Server: - - fwe - body: - encoding: UTF-8 - string: '{"code":"invalid_credentials","message":"You have to be logged in to - perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:44:57 GMT + recorded_at: Tue, 04 Jan 2022 12:44:11 GMT recorded_with: VCR 6.0.0 diff --git a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_hostname.yml b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_hostname.yml index f6b9f81e4..4ec598ecd 100644 --- a/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_hostname.yml +++ b/test/data/vcr_cassettes/system/import/freshdesk/import_freshdesk_fields_validation_valid_hostname.yml @@ -21,7 +21,7 @@ http_interactions: message: Not Found headers: Date: - - Thu, 08 Jul 2021 14:44:49 GMT + - Tue, 04 Jan 2022 12:44:04 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -31,7 +31,7 @@ http_interactions: Status: - 404 Not Found X-Request-Id: - - 0331764a-f490-4d1c-a2ca-474b7c7d3b25 + - ad01f052-b3de-4a2b-bd6b-174d9b696f57 X-Rack-Cache: - miss Cache-Control: @@ -45,13 +45,13 @@ http_interactions: X-Fw-Ratelimiting-Managed: - 'false' X-Envoy-Upstream-Service-Time: - - '31' + - '32' X-Trace-Id: - - 00-03bc675e6fd9c1676c45ca67aef06828-1fe725684acee4f8-00 + - 00-09c3d5f076833268b0f24307042fd611-435154e41b725092-00 Server: - fwe body: encoding: ASCII-8BIT string: " " - recorded_at: Thu, 08 Jul 2021 14:44:50 GMT + recorded_at: Tue, 04 Jan 2022 12:44:04 GMT recorded_with: VCR 6.0.0 diff --git a/test/data/vcr_cassettes/system/import/freshdesk/import_progress_setup.yml b/test/data/vcr_cassettes/system/import/freshdesk/import_progress_setup.yml index 8b3643b97..31bd6c861 100644 --- a/test/data/vcr_cassettes/system/import/freshdesk/import_progress_setup.yml +++ b/test/data/vcr_cassettes/system/import/freshdesk/import_progress_setup.yml @@ -21,7 +21,7 @@ http_interactions: message: Unauthorized headers: Date: - - Thu, 08 Jul 2021 14:45:03 GMT + - Tue, 04 Jan 2022 12:44:17 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -31,7 +31,7 @@ http_interactions: Status: - 401 Unauthorized X-Request-Id: - - 16535d28-0fa9-4bbd-81a5-7734a134dbe6 + - cd0481fb-175a-4720-a94b-eefe4f1bb918 X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -51,20 +51,20 @@ http_interactions: X-Ratelimit-Total: - '40' X-Ratelimit-Remaining: - - '36' + - '39' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '226' + - '38' X-Trace-Id: - - 00-cfd704f35e647fd26907025f0d0227db-0078eeaf66759491-00 + - 00-149154c0cbc8583fee25c248dbcd2f43-be1120345e719f2d-00 Server: - fwe body: encoding: UTF-8 string: '{"code":"invalid_credentials","message":"You have to be logged in to perform this action."}' - recorded_at: Thu, 08 Jul 2021 14:45:03 GMT + recorded_at: Tue, 04 Jan 2022 12:44:17 GMT - request: method: get uri: https://.freshdesk.com/api/v2/agents/me @@ -90,7 +90,7 @@ http_interactions: message: OK headers: Date: - - Thu, 08 Jul 2021 14:45:04 GMT + - Tue, 04 Jan 2022 12:44:18 GMT Content-Type: - application/json; charset=utf-8 Transfer-Encoding: @@ -102,7 +102,7 @@ http_interactions: Pragma: - no-cache X-Request-Id: - - 3613fc6e-8364-44f1-b5ae-e4b92c4f17b6 + - e892def9-1834-47c8-aaf9-606eff0ba571 X-Freshdesk-Api-Version: - latest=v2; requested=v2 X-Rack-Cache: @@ -118,7 +118,7 @@ http_interactions: Expires: - Wed, 13 Oct 2010 00:00:00 UTC Set-Cookie: - - _helpkit_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJTNmMDYxMzM4YmI1MGE3NjdhMWM5YjMxODJjMDJiYjQ1BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMU9rVTAybEdLVmVCc2ZXaGpERThtNHYyeERURHVrRllZYnRJT3ZkQnN4MHc9BjsARg%3D%3D--362eaa4142cd23acdd978a5d43af16884701199e; + - _helpkit_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFVEkiJWZhMDViYWNmYmExMzMzMGYxMzU4NjVkYzg0YzA5MTUyBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMUQzTnE0aXNoRk1ZUGYrNVZMSXczM2ZWcmxPTUszQ0RhQVpoSmJWWDJWcEk9BjsARg%3D%3D--4f8eb3b59a5fefdd94b8a93109853862eb53224c; path=/; HttpOnly; secure - _x_w=5_2; path=/; HttpOnly; secure X-Fw-Ratelimiting-Managed: @@ -126,18 +126,92 @@ http_interactions: X-Ratelimit-Total: - '100' X-Ratelimit-Remaining: - - '90' + - '98' X-Ratelimit-Used-Currentrequest: - '1' X-Envoy-Upstream-Service-Time: - - '71' + - '59' X-Trace-Id: - - 00-0b4df5d88358f783274f4e709f735c1b-f919d65b8eb09b19-00 + - 00-85c6b8a2dac9fdeec2a484a02908bd44-002eab3aa99e9f1c-00 Server: - fwe body: encoding: ASCII-8BIT string: '{"available":false,"occasional":false,"id":80014400475,"ticket_scope":1,"signature":null,"group_ids":[],"role_ids":[80000198826],"skill_ids":[],"available_since":null,"contact":{"active":true,"email":"info@.org","job_title":null,"language":"en","mobile":null,"name":"Thorsten - Eckel","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:23:58Z","updated_at":"2021-04-09T13:31:00Z","last_login_at":"2021-06-07T05:38:22Z"},"created_at":"2021-04-09T13:23:58Z","updated_at":"2021-07-08T07:08:47Z","type":"support_agent"}' - recorded_at: Thu, 08 Jul 2021 14:45:04 GMT + Eckel","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:23:58Z","updated_at":"2021-04-09T13:31:00Z","last_login_at":"2021-10-07T17:33:11Z"},"created_at":"2021-04-09T13:23:58Z","updated_at":"2022-01-03T18:35:31Z","type":"support_agent"}' + recorded_at: Tue, 04 Jan 2022 12:44:18 GMT +- request: + method: get + uri: https://.freshdesk.com/api/v2/agents + body: + encoding: US-ASCII + string: '' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + Host: + - ".freshdesk.com" + Authorization: + - Basic + response: + status: + code: 200 + message: OK + headers: + Date: + - Tue, 04 Jan 2022 12:44:19 GMT + Content-Type: + - application/json; charset=utf-8 + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Status: + - 200 OK + Pragma: + - no-cache + X-Request-Id: + - db2c5bf7-1f2d-438e-bb4e-781d230ec791 + X-Freshdesk-Api-Version: + - latest=v2; requested=v2 + X-Rack-Cache: + - miss + Cache-Control: + - must-revalidate, no-cache, no-store, private, max-age=0 + X-Xss-Protection: + - 1; mode=block + X-Ua-Compatible: + - IE=Edge,chrome=1 + X-Content-Type-Options: + - nosniff + Expires: + - Wed, 13 Oct 2010 00:00:00 UTC + Set-Cookie: + - _x_w=5_2; path=/; HttpOnly; secure + X-Fw-Ratelimiting-Managed: + - 'true' + X-Ratelimit-Total: + - '100' + X-Ratelimit-Remaining: + - '97' + X-Ratelimit-Used-Currentrequest: + - '1' + X-Envoy-Upstream-Service-Time: + - '57' + X-Trace-Id: + - 00-2cb68ae4553a54506e56d015a155cf40-f61758316b28b76d-00 + Server: + - fwe + body: + encoding: ASCII-8BIT + string: '[{"available":false,"occasional":true,"id":80014400480,"ticket_scope":1,"created_at":"2021-04-09T13:24:01Z","updated_at":"2021-04-09T13:24:01Z","last_active_at":null,"available_since":null,"type":"support_agent","contact":{"active":false,"email":"custserv@freshdesk.com","job_title":null,"language":"en","last_login_at":null,"mobile":null,"name":"Customer + Service","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:24:01Z","updated_at":"2021-04-09T13:24:01Z"},"signature":null},{"available":false,"occasional":false,"id":80014400475,"ticket_scope":1,"created_at":"2021-04-09T13:23:58Z","updated_at":"2022-01-03T18:35:31Z","last_active_at":"2022-01-03T18:35:31Z","available_since":null,"type":"support_agent","contact":{"active":true,"email":"info@.org","job_title":null,"language":"en","last_login_at":"2021-10-07T17:33:11Z","mobile":null,"name":"Thorsten + Eckel","phone":null,"time_zone":"Eastern Time (US & Canada)","created_at":"2021-04-09T13:23:58Z","updated_at":"2021-04-09T13:31:00Z"},"signature":null}]' + recorded_at: Tue, 04 Jan 2022 12:44:19 GMT recorded_with: VCR 6.0.0 diff --git a/test/integration/geo_ip_test.rb b/test/integration/geo_ip_test.rb index bedfa0c77..f205f18b0 100644 --- a/test/integration/geo_ip_test.rb +++ b/test/integration/geo_ip_test.rb @@ -28,11 +28,11 @@ class GeoIpTest < ActiveSupport::TestCase result = Service::GeoIp.location('195.65.29.254') assert(result) assert_equal('Switzerland', result['country_name']) - assert_equal('Regensdorf', result['city_name']) + assert_equal('Niederhasli', result['city_name']) assert_equal('CH', result['country_code']) assert_equal('EU', result['continent_code']) - assert_equal(47.4341, result['latitude']) - assert_equal(8.4687, result['longitude']) + assert_equal(47.4823, result['latitude']) + assert_equal(8.4823, result['longitude']) result = Service::GeoIp.location('134.109.140.74') assert(result) @@ -40,8 +40,8 @@ class GeoIpTest < ActiveSupport::TestCase assert_equal('Chemnitz', result['city_name']) assert_equal('DE', result['country_code']) assert_equal('EU', result['continent_code']) - assert_equal(50.8197, result['latitude']) - assert_equal(12.9403, result['longitude']) + assert_equal(50.8191, result['latitude']) + assert_equal(12.9419, result['longitude']) result = Service::GeoIp.location('46.253.55.170') assert(result) @@ -49,16 +49,16 @@ class GeoIpTest < ActiveSupport::TestCase assert_equal('Halle', result['city_name']) assert_equal('DE', result['country_code']) assert_equal('EU', result['continent_code']) - assert_equal(51.5036, result['latitude']) - assert_equal(11.9594, result['longitude']) + assert_equal(51.4825, result['latitude']) + assert_equal(11.9772, result['longitude']) result = Service::GeoIp.location('169.229.216.200') assert(result) assert_equal('United States', result['country_name']) - assert_equal('Alameda', result['city_name']) + assert_equal('Oakland', result['city_name']) assert_equal('US', result['country_code']) assert_equal('NA', result['continent_code']) - assert_equal(37.7688, result['latitude']) - assert_equal(-122.262, result['longitude']) + assert_equal(37.8376, result['latitude']) + assert_equal(-122.2398, result['longitude']) end end diff --git a/test/integration/otrs_import_test.rb b/test/integration/otrs_import_test.rb index 1111a9f90..e2951153e 100644 --- a/test/integration/otrs_import_test.rb +++ b/test/integration/otrs_import_test.rb @@ -36,7 +36,7 @@ class OtrsImportTest < ActiveSupport::TestCase end.collect do |local_object| local_object['name'] end - expected_object_attribute_names = %w[vertriebsweg te_test sugar_crm_remote_no sugar_crm_company_selected_no sugar_crm_company_selection combine itsm_criticality customer_id itsm_impact itsm_review_required itsm_decision_result itsm_repair_start_time itsm_recovery_start_time itsm_decision_date title itsm_due_date topic_no open_exchange_ticket_number hostname ticket_free_key11 type ticket_free_text11 open_exchange_tn topic zarafa_tn group_id scom_hostname checkbox_example scom_uuid scom_state scom_service location owner_id department customer_location state_id pending_time priority_id tags] + expected_object_attribute_names = %w[vertriebsweg te_test number sugar_crm_remote_no sugar_crm_company_selected_no sugar_crm_company_selection combine title itsm_criticality customer_id itsm_impact itsm_review_required itsm_decision_result organization_id itsm_repair_start_time itsm_recovery_start_time itsm_decision_date itsm_due_date topic_no open_exchange_ticket_number hostname ticket_free_key11 type ticket_free_text11 open_exchange_tn topic zarafa_tn group_id scom_hostname checkbox_example scom_uuid scom_state scom_service location owner_id department customer_location state_id pending_time priority_id tags] assert_equal(expected_object_attribute_names, object_attribute_names, 'dynamic field names') end diff --git a/test/integration/report_test.rb b/test/integration/report_test.rb deleted file mode 100644 index 48db03649..000000000 --- a/test/integration/report_test.rb +++ /dev/null @@ -1,1404 +0,0 @@ -# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ - -require 'integration_test_helper' - -class ReportTest < ActiveSupport::TestCase - include SearchindexHelper - - setup do - - # create attribute - ObjectManager::Attribute.add( - object: 'Ticket', - name: 'test_category', - display: 'Test 1', - data_type: 'tree_select', - data_option: { - maxlength: 200, - null: false, - default: '', - options: [ - { 'name' => 'aa', 'value' => 'aa', 'children' => [{ 'name' => 'aa', 'value' => 'aa::aa' }, { 'name' => 'bb', 'value' => 'aa::bb' }, { 'name' => 'cc', 'value' => 'aa::cc' }] }, - { 'name' => 'bb', 'value' => 'bb', 'children' => [{ 'name' => 'aa', 'value' => 'bb::aa' }, { 'name' => 'bb', 'value' => 'bb::bb' }, { 'name' => 'cc', 'value' => 'bb::cc' }] }, - { 'name' => 'cc', 'value' => 'cc', 'children' => [{ 'name' => 'aa', 'value' => 'cc::aa' }, { 'name' => 'bb', 'value' => 'cc::bb' }, { 'name' => 'cc', 'value' => 'cc::cc' }] }, - ] - }, - active: true, - screens: {}, - position: 20, - created_by_id: 1, - updated_by_id: 1, - editable: false, - to_migrate: false, - ) - ObjectManager::Attribute.migration_execute - - configure_elasticsearch(required: true) - - Ticket.destroy_all - - rebuild_searchindex - - group1 = Group.lookup(name: 'Users') - group2 = Group.create!( - name: 'Report Test', - updated_by_id: 1, - created_by_id: 1 - ) - - @ticket1 = Ticket.create!( - title: 'test 1', - group: group2, - customer_id: 2, - state: Ticket::State.lookup(name: 'new'), - priority: Ticket::Priority.lookup(name: '2 normal'), - created_at: '2015-10-28 09:30:00 UTC', - updated_at: '2015-10-28 09:30:00 UTC', - test_category: 'cc::bb', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket1.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_inbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Customer').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-10-28 09:30:00 UTC', - updated_at: '2015-10-28 09:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - @ticket1.tag_add('aaa', 1) - @ticket1.tag_add('bbb', 1) - @ticket1.update!( - group: Group.lookup(name: 'Users'), - updated_at: '2015-10-28 14:30:00 UTC', - ) - - @ticket2 = Ticket.create!( - title: 'test 2', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'new'), - priority: Ticket::Priority.lookup(name: '2 normal'), - created_at: '2015-10-28 09:30:01 UTC', - updated_at: '2015-10-28 09:30:01 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket2.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_inbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Customer').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-10-28 09:30:01 UTC', - updated_at: '2015-10-28 09:30:01 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - @ticket2.tag_add('aaa', 1) - @ticket2.update!( - group_id: group2.id, - updated_at: '2015-10-28 14:30:00 UTC', - ) - - @ticket3 = Ticket.create!( - title: 'test 3', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'open'), - priority: Ticket::Priority.lookup(name: '3 high'), - created_at: '2015-10-28 10:30:00 UTC', - updated_at: '2015-10-28 10:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket3.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_inbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Customer').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-10-28 10:30:00 UTC', - updated_at: '2015-10-28 10:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - - @ticket4 = Ticket.create!( - title: 'test 4', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'closed'), - priority: Ticket::Priority.lookup(name: '2 normal'), - close_at: '2015-10-28 11:30:00 UTC', - created_at: '2015-10-28 10:30:01 UTC', - updated_at: '2015-10-28 10:30:01 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket4.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_inbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Customer').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-10-28 10:30:01 UTC', - updated_at: '2015-10-28 10:30:01 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - - @ticket5 = Ticket.create!( - title: 'test 5', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'closed'), - priority: Ticket::Priority.lookup(name: '3 high'), - close_at: '2015-10-28 11:40:00 UTC', - created_at: '2015-10-28 11:30:00 UTC', - updated_at: '2015-10-28 11:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket5.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_outbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Agent').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-10-28 11:30:00 UTC', - updated_at: '2015-10-28 11:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - @ticket5.tag_add('bbb', 1) - @ticket5.update!( - state: Ticket::State.lookup(name: 'open'), - updated_at: '2015-10-28 14:30:00 UTC', - ) - - @ticket6 = Ticket.create!( - title: 'test 6', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'closed'), - priority: Ticket::Priority.lookup(name: '2 normal'), - close_at: '2015-10-31 12:35:00 UTC', - created_at: '2015-10-31 12:30:00 UTC', - updated_at: '2015-10-31 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket6.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_outbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Agent').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-10-31 12:30:00 UTC', - updated_at: '2015-10-31 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - - @ticket7 = Ticket.create!( - title: 'test 7', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'closed'), - priority: Ticket::Priority.lookup(name: '2 normal'), - close_at: '2015-11-01 12:30:00 UTC', - created_at: '2015-11-01 12:30:00 UTC', - updated_at: '2015-11-01 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket7.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_outbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Agent').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-11-01 12:30:00 UTC', - updated_at: '2015-11-01 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - - @ticket8 = Ticket.create!( - title: 'test 8', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'merged'), - priority: Ticket::Priority.lookup(name: '2 normal'), - close_at: '2015-11-02 12:30:00 UTC', - created_at: '2015-11-02 12:30:00 UTC', - updated_at: '2015-11-02 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket8.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_outbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Agent').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2015-11-02 12:30:00 UTC', - updated_at: '2015-11-02 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - - @ticket9 = Ticket.create!( - title: 'test 9', - group: group1, - customer_id: 2, - state: Ticket::State.lookup(name: 'open'), - priority: Ticket::Priority.lookup(name: '2 normal'), - close_at: '2037-11-02 12:30:00 UTC', - created_at: '2037-11-02 12:30:00 UTC', - updated_at: '2037-11-02 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - Ticket::Article.create!( - ticket_id: @ticket9.id, - from: 'some_sender@example.com', - to: 'some_recipient@example.com', - subject: 'some subject', - message_id: 'some@id', - body: 'some message article_outbound', - internal: false, - sender: Ticket::Article::Sender.where(name: 'Agent').first, - type: Ticket::Article::Type.where(name: 'email').first, - created_at: '2037-11-02 12:30:00 UTC', - updated_at: '2037-11-02 12:30:00 UTC', - updated_by_id: 1, - created_by_id: 1, - ) - - # execute background jobs - Scheduler.worker(true) - SearchIndexBackend.refresh - end - - test 'compare' do - - # first solution - result = Report::TicketFirstSolution.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(2, result[9]) - assert_equal(1, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketFirstSolution.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_equal(@ticket6.id, result[:ticket_ids][1]) - assert_equal(@ticket7.id, result[:ticket_ids][2]) - assert_nil(result[:ticket_ids][3]) - - # month - with selector #1 - result = Report::TicketFirstSolution.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket.priority_id' => { - 'operator' => 'is', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(1, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketFirstSolution.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket.priority_id' => { - 'operator' => 'is', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # month - with merged tickets selector - result = Report::TicketFirstSolution.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket_state.name' => { - 'operator' => 'is not', - 'value' => 'merged', - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(2, result[9]) - assert_equal(1, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketFirstSolution.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket_state.name' => { - 'operator' => 'is not', - 'value' => 'merged', - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][3]) - - # month - with selector #2 - result = Report::TicketFirstSolution.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket.priority_id' => { - 'operator' => 'is not', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(1, result[9]) - assert_equal(1, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketFirstSolution.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket.priority_id' => { - 'operator' => 'is not', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket6.id, result[:ticket_ids][0]) - assert_equal(@ticket7.id, result[:ticket_ids][1]) - assert_nil(result[:ticket_ids][2]) - - # week - result = Report::TicketFirstSolution.aggs( - range_start: Time.zone.parse('2015-10-26T00:00:00Z'), - range_end: Time.zone.parse('2015-10-31T23:59:59Z'), - interval: 'week', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(1, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(1, result[5]) - assert_equal(1, result[6]) - assert_nil(result[7]) - - result = Report::TicketFirstSolution.items( - range_start: Time.zone.parse('2015-10-26T00:00:00Z'), - range_end: Time.zone.parse('2015-11-01T23:59:59Z'), - interval: 'week', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_equal(@ticket6.id, result[:ticket_ids][1]) - assert_equal(@ticket7.id, result[:ticket_ids][2]) - assert_nil(result[:ticket_ids][3]) - - # day - result = Report::TicketFirstSolution.aggs( - range_start: Time.zone.parse('2015-10-01T00:00:00Z'), - range_end: Time.zone.parse('2015-11-01T23:59:59Z'), - interval: 'day', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(0, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_equal(0, result[12]) - assert_equal(0, result[13]) - assert_equal(0, result[14]) - assert_equal(0, result[15]) - assert_equal(0, result[16]) - assert_equal(0, result[17]) - assert_equal(0, result[18]) - assert_equal(0, result[19]) - assert_equal(0, result[20]) - assert_equal(0, result[21]) - assert_equal(0, result[22]) - assert_equal(0, result[23]) - assert_equal(0, result[24]) - assert_equal(0, result[25]) - assert_equal(0, result[26]) - assert_equal(1, result[27]) - assert_equal(0, result[28]) - assert_equal(0, result[29]) - assert_equal(1, result[30]) - assert_nil(result[31]) - - result = Report::TicketFirstSolution.items( - range_start: Time.zone.parse('2015-10-01T00:00:00Z'), - range_end: Time.zone.parse('2015-10-31T23:59:59Z'), - interval: 'day', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_equal(@ticket6.id, result[:ticket_ids][1]) - assert_nil(result[:ticket_ids][2]) - - # hour - result = Report::TicketFirstSolution.aggs( - range_start: Time.zone.parse('2015-10-28T00:00:00Z'), - range_end: Time.zone.parse('2015-10-28T23:59:59Z'), - interval: 'hour', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(0, result[9]) - assert_equal(0, result[10]) - assert_equal(1, result[11]) - assert_equal(0, result[12]) - assert_equal(0, result[13]) - assert_equal(0, result[14]) - assert_equal(0, result[15]) - assert_equal(0, result[16]) - assert_equal(0, result[17]) - assert_equal(0, result[18]) - assert_equal(0, result[19]) - assert_equal(0, result[20]) - assert_equal(0, result[21]) - assert_equal(0, result[22]) - assert_equal(0, result[23]) - assert_nil(result[24]) - - result = Report::TicketFirstSolution.items( - range_start: Time.zone.parse('2015-10-28T00:00:00Z'), - range_end: Time.zone.parse('2015-10-28T23:59:59Z'), - interval: 'hour', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # reopen - result = Report::TicketReopened.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(1, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketReopened.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: {}, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # month - with selector #1 - result = Report::TicketReopened.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket.priority_id' => { - 'operator' => 'is', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(1, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketReopened.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket.priority_id' => { - 'operator' => 'is', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # month - with selector #2 - result = Report::TicketReopened.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket.priority_id' => { - 'operator' => 'is not', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(0, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketReopened.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket.priority_id' => { - 'operator' => 'is not', - 'value' => [Ticket::Priority.lookup(name: '3 high').id], - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_nil(result[:ticket_ids][0]) - - # month - reopened with merge selector - result = Report::TicketReopened.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket_state.name' => { - 'operator' => 'is not', - 'value' => 'merged', - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(1, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketReopened.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket_state.name' => { - 'operator' => 'is not', - 'value' => 'merged', - } - }, # ticket selector to get only a collection of tickets - ) - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # move in/out without merged status - result = Report::TicketMoved.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket_state.name' => { - 'operator' => 'is not', - 'value' => 'merged', - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'in', - }, - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(0, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketMoved.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket.group_id' => { - 'operator' => 'is', - 'value' => [Group.lookup(name: 'Users').id], - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'in', - }, - ) - assert(result) - assert_equal(@ticket1.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # move in/out - result = Report::TicketMoved.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket.group_id' => { - 'operator' => 'is', - 'value' => [Group.lookup(name: 'Users').id], - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'in', - }, - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(1, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketMoved.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket.group_id' => { - 'operator' => 'is', - 'value' => [Group.lookup(name: 'Users').id], - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'in', - }, - ) - assert(result) - assert_equal(@ticket1.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # out without merged tickets - result = Report::TicketMoved.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket_state.name' => { - 'operator' => 'is not', - 'value' => 'merged', - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'out', - }, - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(0, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketMoved.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket_state.name' => { - 'operator' => 'is not', - 'value' => 'merged', - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'out', - }, - ) - assert(result) - assert_nil(result[:ticket_ids][0]) - - # out - result = Report::TicketMoved.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'ticket.group_id' => { - 'operator' => 'is', - 'value' => [Group.lookup(name: 'Users').id], - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'out', - }, - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(1, result[9]) - assert_equal(0, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketMoved.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'ticket.group_id' => { - 'operator' => 'is', - 'value' => [Group.lookup(name: 'Users').id], - } - }, # ticket selector to get only a collection of tickets - params: { - type: 'out', - }, - ) - assert(result) - assert_equal(@ticket2.id, result[:ticket_ids][0]) - assert_nil(result[:ticket_ids][1]) - - # create at - result = Report::TicketGenericTime.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: {}, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(6, result[9]) - assert_equal(1, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: {}, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket5.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][4].to_i) - assert_equal(@ticket2.id, result[:ticket_ids][5].to_i) - assert_equal(@ticket1.id, result[:ticket_ids][6].to_i) - assert_nil(result[:ticket_ids][7]) - - # create at - selector with merge - result = Report::TicketGenericTime.aggs( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - interval: 'month', # year, quarter, month, week, day, hour, minute, second - selector: { - 'state' => { - 'operator' => 'is not', - 'value' => 'merged' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - assert(result) - assert_equal(0, result[0]) - assert_equal(0, result[1]) - assert_equal(0, result[2]) - assert_equal(0, result[3]) - assert_equal(0, result[4]) - assert_equal(0, result[5]) - assert_equal(0, result[6]) - assert_equal(0, result[7]) - assert_equal(0, result[8]) - assert_equal(6, result[9]) - assert_equal(1, result[10]) - assert_equal(0, result[11]) - assert_nil(result[12]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'state' => { - 'operator' => 'is not', - 'value' => 'merged' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket5.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][4].to_i) - assert_equal(@ticket2.id, result[:ticket_ids][5].to_i) - assert_equal(@ticket1.id, result[:ticket_ids][6].to_i) - assert_nil(result[:ticket_ids][7]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'before (absolute)', - 'value' => '2015-10-31T00:00:00Z' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket2.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket1.id, result[:ticket_ids][4].to_i) - assert_nil(result[:ticket_ids][5]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'after (absolute)', - 'value' => '2015-10-31T00:00:00Z' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_nil(result[:ticket_ids][2]) - - Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'before (relative)', - 'range' => 'day', - 'value' => '1' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'after (relative)', - 'range' => 'day', - 'value' => '1' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_nil(result[:ticket_ids][0]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2037-01-01T00:00:00Z'), - range_end: Time.zone.parse('2037-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'before (relative)', - 'range' => 'day', - 'value' => '1' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_nil(result[:ticket_ids][0]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2037-01-01T00:00:00Z'), - range_end: Time.zone.parse('2037-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'after (relative)', - 'range' => 'day', - 'value' => '5' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket9.id, result[:ticket_ids][0].to_i) - assert_nil(result[:ticket_ids][1]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2037-01-01T00:00:00Z'), - range_end: Time.zone.parse('2037-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'before (relative)', - 'range' => 'month', - 'value' => '1' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_nil(result[:ticket_ids][0]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2037-01-01T00:00:00Z'), - range_end: Time.zone.parse('2037-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'after (relative)', - 'range' => 'month', - 'value' => '5' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket9.id, result[:ticket_ids][0].to_i) - assert_nil(result[:ticket_ids][1]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2037-01-01T00:00:00Z'), - range_end: Time.zone.parse('2037-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'before (relative)', - 'range' => 'year', - 'value' => '1' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_nil(result[:ticket_ids][0]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2037-01-01T00:00:00Z'), - range_end: Time.zone.parse('2037-12-31T23:59:59Z'), - selector: { - 'created_at' => { - 'operator' => 'after (relative)', - 'range' => 'year', - 'value' => '5' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket9.id, result[:ticket_ids][0].to_i) - assert_nil(result[:ticket_ids][1]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains all', - 'value' => 'aaa, bbb' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket1.id, result[:ticket_ids][0].to_i) - assert_nil(result[:ticket_ids][1]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains all not', - 'value' => 'aaa, bbb' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket5.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][4].to_i) - assert_equal(@ticket2.id, result[:ticket_ids][5].to_i) - assert_nil(result[:ticket_ids][6]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains all', - 'value' => 'aaa' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - - assert_equal(@ticket2.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket1.id, result[:ticket_ids][1].to_i) - assert_nil(result[:ticket_ids][2]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains all not', - 'value' => 'aaa' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket5.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][4].to_i) - assert_nil(result[:ticket_ids][5]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains one not', - 'value' => 'aaa' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket5.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][4].to_i) - assert_nil(result[:ticket_ids][5]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains one not', - 'value' => 'aaa, bbb' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][3].to_i) - assert_nil(result[:ticket_ids][4]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains one', - 'value' => 'aaa' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - - assert_equal(@ticket2.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket1.id, result[:ticket_ids][1].to_i) - assert_nil(result[:ticket_ids][2]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'tags' => { - 'operator' => 'contains one', - 'value' => 'aaa, bbb' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket5.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket2.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket1.id, result[:ticket_ids][2].to_i) - assert_nil(result[:ticket_ids][3]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'title' => { - 'operator' => 'contains', - 'value' => 'test' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket7.id, result[:ticket_ids][0].to_i) - assert_equal(@ticket6.id, result[:ticket_ids][1].to_i) - assert_equal(@ticket5.id, result[:ticket_ids][2].to_i) - assert_equal(@ticket4.id, result[:ticket_ids][3].to_i) - assert_equal(@ticket3.id, result[:ticket_ids][4].to_i) - assert_equal(@ticket2.id, result[:ticket_ids][5].to_i) - assert_equal(@ticket1.id, result[:ticket_ids][6].to_i) - assert_nil(result[:ticket_ids][7]) - - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'title' => { - 'operator' => 'contains not', - 'value' => 'test' - } - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_nil(result[:ticket_ids][0]) - - # search for test_category.keyword to find values with :: in query - result = Report::TicketGenericTime.items( - range_start: Time.zone.parse('2015-01-01T00:00:00Z'), - range_end: Time.zone.parse('2015-12-31T23:59:59Z'), - selector: { - 'test_category' => { - 'operator' => 'is', - 'value' => 'cc::bb' - }, - }, # ticket selector to get only a collection of tickets - params: { field: 'created_at' }, - ) - - assert(result) - assert_equal(@ticket1.id, result[:ticket_ids][0].to_i) - assert_nil(result[:ticket_ids][1]) - end - -end diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index c95cb651f..2abf0cdb9 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -170,8 +170,8 @@ class UserTest < ActiveSupport::TestCase update_verify: { firstname: 'Bob', lastname: 'Smith', - image: 'a11ed3970e6d3a680527d6f3f075ff89', - image_md5: 'a11ed3970e6d3a680527d6f3f075ff89', + image: '7c3af305038fc695a9563eda2eb78f57', + image_md5: '7c3af305038fc695a9563eda2eb78f57', email: 'unit-test1@znuny.com', login: 'login-4', } @@ -189,8 +189,8 @@ class UserTest < ActiveSupport::TestCase create_verify: { firstname: 'Bob', lastname: 'Smith', - image: 'd76099edb79f39624b35187873184e3c', - image_md5: 'd76099edb79f39624b35187873184e3c', + image: 'cc98289b7af056fbd00ff0c1d08284c4', + image_md5: 'cc98289b7af056fbd00ff0c1d08284c4', email: 'unit-test2@znuny.com', login: 'login-5', }, @@ -200,8 +200,8 @@ class UserTest < ActiveSupport::TestCase update_verify: { firstname: 'Bob', lastname: 'Smith', - image: 'a11ed3970e6d3a680527d6f3f075ff89', - image_md5: 'a11ed3970e6d3a680527d6f3f075ff89', + image: '7c3af305038fc695a9563eda2eb78f57', + image_md5: '7c3af305038fc695a9563eda2eb78f57', email: 'unit-test1@znuny.com', login: 'login-5', }