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 @@
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 @@
from
- till
-
+ till
+
<% 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') %>
-
+
@@ -16,25 +16,51 @@
<%- @T('First Response Time') %>
<%- @T('Timeframe for the first response.') %>
-
+
-
+
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
-
- <%- @T('Update Time') %>
- <%- @T('Timeframe for every following response.') %>
-
-
+
+
<%- @T('Update Time') %>
+
<%- @T('Timeframe between agent updates or for an agent to respond.') %>
+
-
+
+
+ checked<% end %>>
+ <%- @Icon('radio', 'icon-unchecked') %>
+ <%- @Icon('radio-checked', 'icon-checked') %>
+ <%- @T('between agent updates') %>
+
+
+
+
+
+ checked<% end %>>
+ <%- @Icon('radio', 'icon-unchecked') %>
+ <%- @Icon('radio-checked', 'icon-checked') %>
+ <%- @T('for an agent to respond') %>
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -44,7 +70,7 @@
<%- @T('Solution Time') %>
<%- @T('Timeframe for solving the problem.') %>
-
+
diff --git a/app/assets/javascripts/app/views/integration/cti.jst.eco b/app/assets/javascripts/app/views/integration/cti.jst.eco
index 3c773dba5..4cdbff6d0 100644
--- a/app/assets/javascripts/app/views/integration/cti.jst.eco
+++ b/app/assets/javascripts/app/views/integration/cti.jst.eco
@@ -82,7 +82,7 @@
- <%- @T('Name') %>
+ <%- @T('Value') %>
<%- @T('Description') %>
diff --git a/app/assets/javascripts/app/views/integration/placetel.jst.eco b/app/assets/javascripts/app/views/integration/placetel.jst.eco
index 48d939b51..273a87497 100644
--- a/app/assets/javascripts/app/views/integration/placetel.jst.eco
+++ b/app/assets/javascripts/app/views/integration/placetel.jst.eco
@@ -91,24 +91,30 @@
+-->
- <%- @T('Default caller id.') %>
+
<%- @T('Settings') %>
- <%- @T('Default caller id') %>
- <%- @T('Note') %>
+ <%- @T('Value') %>
+ <%- @T('Description') %>
+
+
+
+ <%- @T('Shown records in caller log.') %>
--->
+
<%- @T('User assignment to telephones') %>
<%- @T('User assignment to telephones to be able to offer extended like open new ticket screen on answering a call.') %>
@@ -137,4 +143,4 @@
<%- @T('Save') %>
-
\ No newline at end of file
+
diff --git a/app/assets/javascripts/app/views/integration/sipgate.jst.eco b/app/assets/javascripts/app/views/integration/sipgate.jst.eco
index 245891f79..d9a2a84dd 100644
--- a/app/assets/javascripts/app/views/integration/sipgate.jst.eco
+++ b/app/assets/javascripts/app/views/integration/sipgate.jst.eco
@@ -79,38 +79,22 @@
-
<%- @T('Default caller id.') %>
+
<%- @T('Settings') %>
-
- <%- @T('In order for Zammad to access %s, a %s API user and password must be stored here', 'Sipgate', 'Sipgate') %>:
-
@@ -143,4 +127,4 @@
<%- @T('Save') %>
-
\ No newline at end of file
+
diff --git a/app/assets/javascripts/app/views/object_manager/index.jst.eco b/app/assets/javascripts/app/views/object_manager/index.jst.eco
index b984dc0c0..3190716d8 100644
--- a/app/assets/javascripts/app/views/object_manager/index.jst.eco
+++ b/app/assets/javascripts/app/views/object_manager/index.jst.eco
@@ -55,6 +55,7 @@
<%- @T('Display') %>
<%- @T('Name') %>
<%- @T('Type') %>
+ <%- @T('Position') %>
<%- @T('Action') %>
@@ -64,6 +65,7 @@
<%= item.display %>
<%= item.name %>
<%= item.data_type %>
+ <%= item.position %>
<% if item.to_create is true: %>
<%- @T('will be created') %>
diff --git a/app/assets/javascripts/app/views/popover/organization.jst.eco b/app/assets/javascripts/app/views/popover/organization.jst.eco
index a8f40ba47..02572e6c3 100644
--- a/app/assets/javascripts/app/views/popover/organization.jst.eco
+++ b/app/assets/javascripts/app/views/popover/organization.jst.eco
@@ -3,7 +3,7 @@
<% if @object.member_ids: %>
-
<%- @T('Members') %>
+
<%- @T('Members') %> (<%= @object.member_ids.length %>)
<% end %>
diff --git a/app/assets/javascripts/app/views/sla/index.jst.eco b/app/assets/javascripts/app/views/sla/index.jst.eco
index 2f7adce89..0aab1d7c5 100644
--- a/app/assets/javascripts/app/views/sla/index.jst.eco
+++ b/app/assets/javascripts/app/views/sla/index.jst.eco
@@ -34,15 +34,19 @@
<%- @T('Escalation Times') %>
<% if sla.first_response_time: %>
- <%- sla.first_response_time_in_text %> <%- @T('hours') %> - <%- @T('First Response Time') %>
+ <%- @time_duration_hh_mm(sla.first_response_time) %> <%- @T('hours') %> - <%- @T('First Response Time') %>
+ <% end %>
+ <% if sla.response_time: %>
+
+ <%- @time_duration_hh_mm(sla.response_time) %> <%- @T('hours') %> - <%- @T('Response Time') %>
<% end %>
<% if sla.update_time: %>
- <%- sla.update_time_in_text %> <%- @T('hours') %> - <%- @T('Update Time') %>
+ <%- @time_duration_hh_mm(sla.update_time) %> <%- @T('hours') %> - <%- @T('Update Time') %>
<% end %>
<% if sla.solution_time: %>
- <%- sla.solution_time_in_text %> <%- @T('hours') %> - <%- @T('Solution Time') %>
+ <%- @time_duration_hh_mm(sla.solution_time) %> <%- @T('hours') %> - <%- @T('Solution Time') %>
<% end %>
@@ -57,4 +61,4 @@
<% end %>
-
\ No newline at end of file
+
diff --git a/app/assets/stylesheets/font.css b/app/assets/stylesheets/font.css
index 566c3e7bd..939bfd96d 100755
--- a/app/assets/stylesheets/font.css
+++ b/app/assets/stylesheets/font.css
@@ -1,39 +1,39 @@
@font-face {
font-family: 'Fira Sans';
- src: url('fonts/FiraSans-Bold.eot');
- src: url('fonts/FiraSans-Bold.woff2') format('woff2'),
- url('fonts/FiraSans-Bold.woff') format('woff'),
- url('fonts/FiraSans-Bold.ttf') format('truetype');
+ src: url('assets/fonts/FiraSans-Bold.eot');
+ src: url('assets/fonts/FiraSans-Bold.woff2') format('woff2'),
+ url('assets/fonts/FiraSans-Bold.woff') format('woff'),
+ url('assets/fonts/FiraSans-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
}
@font-face {
font-family: 'Fira Sans';
- src: url('fonts/FiraSans-Regular.eot');
- src: url('fonts/FiraSans-Regular.woff2') format('woff2'),
- url('fonts/FiraSans-Regular.woff') format('woff'),
- url('fonts/FiraSans-Regular.ttf') format('truetype');
+ src: url('assets/fonts/FiraSans-Regular.eot');
+ src: url('assets/fonts/FiraSans-Regular.woff2') format('woff2'),
+ url('assets/fonts/FiraSans-Regular.woff') format('woff'),
+ url('assets/fonts/FiraSans-Regular.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Fira Sans';
- src: url('fonts/FiraSans-Medium.eot');
- src: url('fonts/FiraSans-Medium.woff2') format('woff2'),
- url('fonts/FiraSans-Medium.woff') format('woff'),
- url('fonts/FiraSans-Medium.ttf') format('truetype');
+ src: url('assets/fonts/FiraSans-Medium.eot');
+ src: url('assets/fonts/FiraSans-Medium.woff2') format('woff2'),
+ url('assets/fonts/FiraSans-Medium.woff') format('woff'),
+ url('assets/fonts/FiraSans-Medium.ttf') format('truetype');
font-weight: 500;
font-style: normal;
}
@font-face {
font-family: 'Fira Sans';
- src: url('fonts/FiraSans-Light.eot');
- src: url('fonts/FiraSans-Light.woff2') format('woff2'),
- url('fonts/FiraSans-Light.woff') format('woff'),
- url('fonts/FiraSans-Light.ttf') format('truetype');
+ src: url('assets/fonts/FiraSans-Light.eot');
+ src: url('assets/fonts/FiraSans-Light.woff2') format('woff2'),
+ url('assets/fonts/FiraSans-Light.woff') format('woff'),
+ url('assets/fonts/FiraSans-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
-}
\ No newline at end of file
+}
diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss
index 025d2a488..e6e591aa6 100644
--- a/app/assets/stylesheets/zammad.scss
+++ b/app/assets/stylesheets/zammad.scss
@@ -358,6 +358,26 @@ ol, ul {
z-index: 1;
}
+code {
+ background: hsla(0, 0%, 0%, 0.2);
+ border-radius: 3px;
+ box-decoration-break: clone;
+}
+
+code,
+.hljs {
+ padding: 2px 4px;
+ font-size: 0.88em;
+}
+
+.hljs {
+ background: none;
+}
+
+pre code.hljs {
+ font-size: 1em;
+}
+
pre {
display: block;
padding: 9.5px;
@@ -371,30 +391,26 @@ pre {
border: 1px solid hsl(0,0%,90%);
border-radius: 3px;
}
+
+.modal-content pre {
+ background: hsl(0, 0%, 97%);
+ border: 1px solid hsl(0, 0%, 87%);
+}
+
pre code {
padding: 0;
font-size: inherit;
color: inherit;
white-space: pre-wrap;
- background-color: transparent;
- border-radius: 0;
-}
-
-.hljs,
-code {
background: none;
- padding: 2px 4px;
- font-size: 0.88em;
-}
+ border-radius: 0;
+ border: none;
+ overflow-x: auto;
-code:not(.hljs) {
- border: 1px solid rgba(0,0,0,.2);
- border-radius: 3px;
- white-space: nowrap;
-}
-
-pre code.hljs {
- font-size: 1em;
+ &.hljs {
+ padding: 0;
+ background: none;
+ }
}
.textarea::placeholder,
@@ -13157,3 +13173,10 @@ span.is-disabled {
.text-modules-box {
max-height: 40vh;
}
+
+.sla_times {
+ .sla_radio_container {
+ padding-top: 0.5em;
+ padding-left: 0.5em;
+ }
+}
diff --git a/app/controllers/application_controller/authenticates.rb b/app/controllers/application_controller/authenticates.rb
index 503649b2f..54a72668a 100644
--- a/app/controllers/application_controller/authenticates.rb
+++ b/app/controllers/application_controller/authenticates.rb
@@ -43,6 +43,10 @@ module ApplicationController::Authenticates
end
def authentication_check_only(auth_param = {})
+ if Rails.env.test? && ENV['FAKE_SELENIUM_LOGIN_USER_ID'].present? && session[:user_id].blank?
+ session[:user_id] = ENV['FAKE_SELENIUM_LOGIN_USER_ID'].to_i
+ end
+
# logger.debug 'authentication_check'
# logger.debug params.inspect
# logger.debug session.inspect
diff --git a/app/controllers/calendars_controller.rb b/app/controllers/calendars_controller.rb
index 0092cdf53..38863730a 100644
--- a/app/controllers/calendars_controller.rb
+++ b/app/controllers/calendars_controller.rb
@@ -38,6 +38,7 @@ class CalendarsController < ApplicationController
end
def destroy
+ model_references_check(Calendar, params)
model_destroy_render(Calendar, params)
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index cae0d5425..c18d4ff48 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -149,6 +149,7 @@ curl http://localhost/api/v1/groups/{id} -v -u #{login}:#{password} -H "Content-
=end
def destroy
+ model_references_check(Group, params)
model_destroy_render(Group, params)
end
end
diff --git a/app/controllers/import_freshdesk_controller.rb b/app/controllers/import_freshdesk_controller.rb
index 96b0ae0aa..fe2b3eb77 100644
--- a/app/controllers/import_freshdesk_controller.rb
+++ b/app/controllers/import_freshdesk_controller.rb
@@ -50,9 +50,9 @@ class ImportFreshdeskController < ApplicationController
Setting.set('import_freshdesk_endpoint_key', params[:token])
- result = Sequencer.process('Import::Freshdesk::ConnectionTest')
+ connection_result = Sequencer.process('Import::Freshdesk::ConnectionTest')
- if !result[:connected]
+ if !connection_result[:connected]
Setting.set('import_freshdesk_endpoint_key', nil)
@@ -63,6 +63,19 @@ class ImportFreshdeskController < ApplicationController
return
end
+ permission_result = Sequencer.process('Import::Freshdesk::PermissionCheck')
+
+ if !permission_result[:permission_present]
+
+ Setting.set('import_freshdesk_endpoint_key', nil)
+
+ render json: {
+ result: 'invalid',
+ message_human: 'Missing administrator permission!',
+ }
+ return
+ end
+
render json: {
result: 'ok',
}
diff --git a/app/controllers/integration/smime_controller.rb b/app/controllers/integration/smime_controller.rb
index ae7a6c621..7204cc842 100644
--- a/app/controllers/integration/smime_controller.rb
+++ b/app/controllers/integration/smime_controller.rb
@@ -42,9 +42,7 @@ class Integration::SMIMEController < ApplicationController
string = params[:file].read.force_encoding('utf-8')
end
- items = string.scan(%r{.+?-+END(?: TRUSTED)? CERTIFICATE-+}mi).each_with_object([]) do |cert, result|
- result << SMIMECertificate.create!(public_key: cert)
- end
+ items = SMIMECertificate.create_certificates(string)
render json: {
result: 'ok',
@@ -73,14 +71,8 @@ class Integration::SMIMEController < ApplicationController
raise "Parameter 'data' or 'file' required." if string.blank?
- private_key = OpenSSL::PKey.read(string, params[:secret])
- modulus = private_key.public_key.n.to_s(16)
-
- certificate = SMIMECertificate.find_by(modulus: modulus)
-
- raise Exceptions::UnprocessableEntity, 'Unable for find certificate for this private key.' if !certificate
-
- certificate.update!(private_key: string, private_key_secret: params[:secret])
+ SMIMECertificate.create_certificates(string)
+ SMIMECertificate.create_private_keys(string, params[:secret])
render json: {
result: 'ok',
diff --git a/app/controllers/object_manager_attributes_controller.rb b/app/controllers/object_manager_attributes_controller.rb
index 864cc2a4d..b645d7df7 100644
--- a/app/controllers/object_manager_attributes_controller.rb
+++ b/app/controllers/object_manager_attributes_controller.rb
@@ -29,7 +29,7 @@ class ObjectManagerAttributesController < ApplicationController
)
raise Exceptions::UnprocessableEntity, 'already exists' if exists
- add_attribute_using_params(permitted_params.merge(position: 1550), status: :created)
+ add_attribute_using_params(permitted_params, status: :created)
end
# PUT /object_manager_attributes/1
diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb
index dfcabde2a..448f8e021 100644
--- a/app/controllers/reports_controller.rb
+++ b/app/controllers/reports_controller.rb
@@ -22,26 +22,34 @@ class ReportsController < ApplicationController
get_params = params_all
return if !get_params
- result = {}
- get_params[:metric][:backend].each do |backend|
- condition = get_params[:profile].condition
- if backend[:condition]
- backend[:condition].merge(condition)
- else
- backend[:condition] = condition
- end
- next if !backend[:adapter]
+ begin
+ result = {}
+ get_params[:metric][:backend].each do |backend|
+ condition = get_params[:profile].condition
+ if backend[:condition]
+ backend[:condition].merge(condition)
+ else
+ backend[:condition] = condition
+ end
+ next if !backend[:adapter]
- result[backend[:name]] = backend[:adapter].aggs(
- range_start: get_params[:start],
- range_end: get_params[:stop],
- interval: get_params[:range],
- selector: backend[:condition],
- params: backend[:params],
- timezone: get_params[:timezone],
- timezone_offset: get_params[:timezone_offset],
- current_user: current_user
- )
+ result[backend[:name]] = backend[:adapter].aggs(
+ range_start: get_params[:start],
+ range_end: get_params[:stop],
+ interval: get_params[:range],
+ selector: backend[:condition],
+ params: backend[:params],
+ timezone: get_params[:timezone],
+ timezone_offset: get_params[:timezone_offset],
+ current_user: current_user
+ )
+ end
+ rescue => e
+ if e.message.include? 'Conflicting date range'
+ raise Exceptions::UnprocessableEntity, 'Conflicting date ranges. Please check your selected report profile.'
+ end
+
+ raise e
end
render json: {
diff --git a/app/jobs/ticket_create_screen_job.rb b/app/jobs/ticket_create_screen_job.rb
deleted file mode 100644
index a7b4372d8..000000000
--- a/app/jobs/ticket_create_screen_job.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
-
-class TicketCreateScreenJob < ApplicationJob
- include HasActiveJobLock
-
- def perform
- Sessions.list.each do |client_id, data|
- next if client_id.blank?
-
- user_id = data&.dig(:user, 'id')
- next if user_id.blank?
-
- user = User.lookup(id: user_id)
- next if !user&.permissions?('ticket.agent')
-
- # get attributes to update
- ticket_create_attributes = Ticket::ScreenOptions.attributes_to_change(
- current_user: user,
- )
-
- # no data exists
- next if ticket_create_attributes.blank?
-
- Rails.logger.debug { "push ticket_create for user #{user.id}" }
- Sessions.send(client_id, {
- event: 'ticket_create_attributes',
- data: ticket_create_attributes,
- })
- end
- end
-end
diff --git a/app/models/calendar.rb b/app/models/calendar.rb
index 24ef0497e..bc27e39bc 100644
--- a/app/models/calendar.rb
+++ b/app/models/calendar.rb
@@ -373,18 +373,14 @@ returns
end
# check if sla's are refer to an existing calendar
- default_calendar = Calendar.find_by(default: true)
- Sla.find_each do |sla|
- if !sla.calendar_id
- sla.calendar_id = default_calendar.id
- sla.save!
- next
- end
- if !Calendar.exists?(id: sla.calendar_id)
+ if destroyed?
+ default_calendar = Calendar.find_by(default: true)
+ Sla.where(calendar_id: id).find_each do |sla|
sla.calendar_id = default_calendar.id
sla.save!
end
end
+
true
end
diff --git a/app/models/channel/driver/sms/message_bird.rb b/app/models/channel/driver/sms/message_bird.rb
index 2efb7052e..8f52c7099 100644
--- a/app/models/channel/driver/sms/message_bird.rb
+++ b/app/models/channel/driver/sms/message_bird.rb
@@ -17,7 +17,7 @@ class Channel::Driver::Sms::MessageBird < Channel::Driver::Sms::Base
send_create(options, attr)
true
rescue => e
- Rails.logger.debug { "MessageBird error: #{e.inspect}" }
+ Rails.logger.error { "MessageBird error: #{e.inspect}" }
raise e
end
end
diff --git a/app/models/channel/email_parser.rb b/app/models/channel/email_parser.rb
index 9c132dc48..2ae902c40 100644
--- a/app/models/channel/email_parser.rb
+++ b/app/models/channel/email_parser.rb
@@ -606,6 +606,20 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
if mail.html_part&.body.present?
content_type = mail.html_part.mime_type || 'text/plain'
body = body_text(mail.html_part, strict_html: true)
+ elsif mail.text_part.present? && mail.all_parts.any? { |elem| elem.inline? && elem.content_type&.start_with?('image') }
+ content_type = 'text/html'
+
+ body = mail
+ .all_parts
+ .reduce('') do |memo, part|
+ if part.mime_type == 'text/plain' && !part.attachment?
+ memo += body_text(part, strict_html: false).text2html
+ elsif part.inline? && part.content_type&.start_with?('image')
+ memo += " "
+ end
+
+ memo
+ end
elsif mail.text_part.present?
content_type = 'text/plain'
@@ -614,9 +628,6 @@ process unprocessable_mails (tmp/unprocessable_mail/*.eml) again
.reduce('') do |memo, part|
if part.mime_type == 'text/plain' && !part.attachment?
memo += body_text(part, strict_html: false)
- elsif part.inline? && part.content_type&.start_with?('image')
- content_type = 'text/html'
- memo += " "
end
memo
diff --git a/app/models/concerns/has_group_relation_definition.rb b/app/models/concerns/has_group_relation_definition.rb
index a91845952..1f05b99f6 100644
--- a/app/models/concerns/has_group_relation_definition.rb
+++ b/app/models/concerns/has_group_relation_definition.rb
@@ -14,8 +14,7 @@ module HasGroupRelationDefinition
validates :access, presence: true
validate :validate_access
- after_save :touch_related
- after_destroy :touch_related
+ after_commit :touch_related
end
private
diff --git a/app/models/concerns/has_groups.rb b/app/models/concerns/has_groups.rb
index 0a4f5c94b..b4764623c 100644
--- a/app/models/concerns/has_groups.rb
+++ b/app/models/concerns/has_groups.rb
@@ -251,7 +251,6 @@ module HasGroups
yield
self.group_access_buffer = nil
cache_delete
- push_ticket_create_screen_background_job
end
def process_group_access_buffer
diff --git a/app/models/concerns/has_roles.rb b/app/models/concerns/has_roles.rb
index 3589cdfa5..5a152beb3 100644
--- a/app/models/concerns/has_roles.rb
+++ b/app/models/concerns/has_roles.rb
@@ -6,9 +6,9 @@ module HasRoles
included do
has_and_belongs_to_many :roles,
before_add: %i[validate_agent_limit_by_role validate_roles],
- after_add: %i[cache_update check_notifications push_ticket_create_screen_for_role_change],
+ after_add: %i[cache_update check_notifications],
before_remove: :last_admin_check_by_role,
- after_remove: %i[cache_update push_ticket_create_screen_for_role_change]
+ after_remove: %i[cache_update]
end
# Checks a given Group( ID) for given access(es) for the instance associated roles.
@@ -45,15 +45,6 @@ module HasRoles
)
end
- def push_ticket_create_screen_for_role_change(role)
- return if Setting.get('import_mode')
-
- permission = Permission.lookup(name: 'ticket.agent')
- return if !role.permissions.exists?(id: permission.id)
-
- push_ticket_create_screen_background_job
- end
-
# methods defined here are going to extend the class, not the instance of it
class_methods do
diff --git a/app/models/concerns/has_search_index_backend.rb b/app/models/concerns/has_search_index_backend.rb
index ede105c55..3a634c059 100644
--- a/app/models/concerns/has_search_index_backend.rb
+++ b/app/models/concerns/has_search_index_backend.rb
@@ -216,23 +216,26 @@ reload search index with full data
def search_index_reload
tolerance = 10
tolerance_count = 0
- batch_size = 100
- query = all.order(created_at: :desc)
+ query = order(created_at: :desc)
total = query.count
- query.find_in_batches(batch_size: batch_size).with_index do |group, batch|
- group.each do |item|
- next if item.ignore_search_indexing?(:destroy)
-
+ record_count = 0
+ batch_size = 100
+ query.as_batches(size: batch_size) do |record|
+ if !record.ignore_search_indexing?(:destroy)
begin
- item.search_index_update_backend
+ record.search_index_update_backend
rescue => e
- logger.error "Unable to send #{item.class}.find(#{item.id}).search_index_update_backend backend: #{e.inspect}"
+ logger.error "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}"
tolerance_count += 1
sleep 15
- raise "Unable to send #{item.class}.find(#{item.id}).search_index_update_backend backend: #{e.inspect}" if tolerance_count == tolerance
+ raise "Unable to send #{record.class}.find(#{record.id}).search_index_update_backend backend: #{e.inspect}" if tolerance_count == tolerance
end
end
- puts "\t#{[(batch + 1) * batch_size, total].min}/#{total}" # rubocop:disable Rails/Output
+
+ record_count += 1
+ if (record_count % batch_size).zero? || record_count == total
+ puts "\t#{record_count}/#{total}" # rubocop:disable Rails/Output
+ end
end
end
end
diff --git a/app/models/concerns/has_ticket_create_screen_impact.rb b/app/models/concerns/has_ticket_create_screen_impact.rb
deleted file mode 100644
index f86742bfb..000000000
--- a/app/models/concerns/has_ticket_create_screen_impact.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
-
-module HasTicketCreateScreenImpact
- extend ActiveSupport::Concern
-
- included do
- after_commit :push_ticket_create_screen
- end
-
- def push_ticket_create_screen?
- return true if destroyed?
-
- %w[id name active updated_at].any? do |attribute|
- saved_change_to_attribute?(attribute)
- end
- end
-
- def push_ticket_create_screen
- return if Setting.get('import_mode')
- return if !push_ticket_create_screen?
-
- push_ticket_create_screen_background_job
- end
-
- def push_ticket_create_screen_background_job
- TicketCreateScreenJob.set(wait: 10.seconds).perform_later
- end
-end
diff --git a/app/models/core_workflow/attributes.rb b/app/models/core_workflow/attributes.rb
index 7005155af..8c121fea2 100644
--- a/app/models/core_workflow/attributes.rb
+++ b/app/models/core_workflow/attributes.rb
@@ -56,22 +56,42 @@ class CoreWorkflow::Attributes
result
end
- def selected
- if @payload['params']['id'] && payload_class.exists?(id: @payload['params']['id'])
- result = saved_only
+ def exists?
+ return if @payload['params']['id'].blank?
+
+ @exists ||= payload_class.exists?(id: @payload['params']['id'])
+ end
+
+ def overwritten
+
+ # params loading and preparing is very expensive so cache it
+ checksum = Digest::MD5.hexdigest(Marshal.dump(@payload['params']))
+ return @overwritten[checksum] if @overwritten.present? && @overwritten[checksum]
+
+ @overwritten = {}
+ @overwritten[checksum] = begin
+ result = saved_only(dump: true)
overwrite_selected(result)
+ end
+ end
+
+ def selected
+ if exists?
+ overwritten
else
selected_only
end
end
- def saved_only
- return if @payload['params']['id'].blank?
+ def saved_only(dump: false)
+ return if !exists?
# dont use lookup here because the cache will not
# know about new attributes and make crashes
@saved_only ||= payload_class.find_by(id: @payload['params']['id'])
+ return @saved_only if !dump
+
# we use marshal here because clone still uses references and dup can't
# detect changes for the rails object
Marshal.load(Marshal.dump(@saved_only))
@@ -193,7 +213,7 @@ class CoreWorkflow::Attributes
end
def attribute_options_relation?(attribute)
- attribute[:relation].present?
+ attribute[:tag] == 'select' && attribute[:relation].present?
end
def values(attribute)
@@ -238,13 +258,10 @@ class CoreWorkflow::Attributes
end
def saved_attribute_value(attribute)
- saved_attribute_value = saved_only&.try(attribute[:name])
# special case for owner_id
- if saved_only&.class == Ticket && attribute[:name] == 'owner_id' && saved_attribute_value == 1
- saved_attribute_value = nil
- end
+ return if saved_only&.class == Ticket && attribute[:name] == 'owner_id'
- saved_attribute_value
+ saved_only&.try(attribute[:name])
end
end
diff --git a/app/models/core_workflow/attributes/user.rb b/app/models/core_workflow/attributes/user.rb
index 4700c0e46..36d3aee11 100644
--- a/app/models/core_workflow/attributes/user.rb
+++ b/app/models/core_workflow/attributes/user.rb
@@ -3,8 +3,11 @@
class CoreWorkflow::Attributes::User < CoreWorkflow::Attributes::Base
def values
- return ticket_owner_id_bulk if @attributes.payload['screen'] == 'overview_bulk'
- return ticket_owner_id if @attributes.payload['class_name'] == 'Ticket' && @attribute[:name] == 'owner_id'
+ if @attribute[:name] == 'owner_id' && @attributes.payload['class_name'] == 'Ticket'
+ return ticket_owner_id_bulk if @attributes.payload['screen'] == 'overview_bulk'
+
+ return ticket_owner_id
+ end
[]
end
@@ -35,7 +38,10 @@ class CoreWorkflow::Attributes::User < CoreWorkflow::Attributes::Base
def ticket_owner_id
return [''] if @attributes.selected_only.group_id.blank?
- group_owner_ids
+ owner_ids = group_owner_ids
+ return [''] if owner_ids.blank?
+
+ owner_ids
end
def group_owner_ids
diff --git a/app/models/core_workflow/condition.rb b/app/models/core_workflow/condition.rb
index f2dd9676b..01dc07341 100644
--- a/app/models/core_workflow/condition.rb
+++ b/app/models/core_workflow/condition.rb
@@ -8,7 +8,7 @@ class CoreWorkflow::Condition
def initialize(result_object:, workflow:)
@user = result_object.user
@payload = result_object.payload
- @workflow = workflow
+ @workflow = workflow
@attribute_object = result_object.attributes
@result_object = result_object
@check = nil
diff --git a/app/models/core_workflow/condition/backend.rb b/app/models/core_workflow/condition/backend.rb
index fa7e474b2..b3bcd7d9c 100644
--- a/app/models/core_workflow/condition/backend.rb
+++ b/app/models/core_workflow/condition/backend.rb
@@ -10,6 +10,10 @@ class CoreWorkflow::Condition::Backend
attr_reader :value
+ def field
+ @key.sub(%r{.*\.}, '')
+ end
+
def object?(object)
@condition_object.attributes.instance_of?(object)
end
diff --git a/app/models/core_workflow/condition/changed_to.rb b/app/models/core_workflow/condition/changed_to.rb
new file mode 100644
index 000000000..4e5827563
--- /dev/null
+++ b/app/models/core_workflow/condition/changed_to.rb
@@ -0,0 +1,9 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class CoreWorkflow::Condition::ChangedTo < CoreWorkflow::Condition::Backend
+ def match
+ return if !CoreWorkflow::Condition::HasChanged.new(condition_object: @condition_object, key: @key, condition: @condition, value: @value).match
+
+ CoreWorkflow::Condition::Is.new(condition_object: @condition_object, key: @key, condition: @condition, value: @value).match
+ end
+end
diff --git a/app/models/core_workflow/condition/has_changed.rb b/app/models/core_workflow/condition/has_changed.rb
new file mode 100644
index 000000000..150233d1a
--- /dev/null
+++ b/app/models/core_workflow/condition/has_changed.rb
@@ -0,0 +1,9 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class CoreWorkflow::Condition::HasChanged < CoreWorkflow::Condition::Backend
+ def match
+ return if @condition_object.payload['last_changed_attribute'] != field
+
+ true
+ end
+end
diff --git a/app/models/core_workflow/custom/admin_sla.rb b/app/models/core_workflow/custom/admin_sla.rb
index 23702b0b6..c8fdce415 100644
--- a/app/models/core_workflow/custom/admin_sla.rb
+++ b/app/models/core_workflow/custom/admin_sla.rb
@@ -16,7 +16,13 @@ class CoreWorkflow::Custom::AdminSla < CoreWorkflow::Custom::Backend
end
def update_time_enabled
- return 'set_mandatory' if params['update_time_enabled'].present?
+ return 'set_mandatory' if params['update_time_enabled'].present? && params['update_type'] == 'update'
+
+ 'set_optional'
+ end
+
+ def response_time_enabled
+ return 'set_mandatory' if params['update_time_enabled'].present? && params['update_type'] == 'response'
'set_optional'
end
@@ -32,6 +38,7 @@ class CoreWorkflow::Custom::AdminSla < CoreWorkflow::Custom::Backend
# make fields mandatory if checkbox is checked
result(first_response_time_enabled, 'first_response_time_in_text')
result(update_time_enabled, 'update_time_in_text')
+ result(response_time_enabled, 'response_time_in_text')
result(solution_time_enabled, 'solution_time_in_text')
end
end
diff --git a/app/models/core_workflow/result.rb b/app/models/core_workflow/result.rb
index 8d057f945..acb39af87 100644
--- a/app/models/core_workflow/result.rb
+++ b/app/models/core_workflow/result.rb
@@ -35,7 +35,12 @@ class CoreWorkflow::Result
# restrict init defaults to make sure param values to removed if not allowed
attributes.restrict_values_default.each do |field, values|
- run_backend_value('set_fixed_to', field, values)
+
+ # skip initial rerun to improve performance
+ # priority e.g. would trigger a rerun because its not set yet
+ # but we skip rerun here because the initial values have no logic which
+ # are dependent on form changes
+ run_backend_value('set_fixed_to', field, values, skip_rerun: true)
end
set_default_only_shown_if_selectable
@@ -89,21 +94,21 @@ class CoreWorkflow::Result
end
end
- def run_backend(field, perform_config)
+ def run_backend(field, perform_config, skip_rerun: false)
result = []
Array(perform_config['operator']).each do |backend|
- result << "CoreWorkflow::Result::#{backend.classify}".constantize.new(result_object: self, field: field, perform_config: perform_config).run
+ result << "CoreWorkflow::Result::#{backend.classify}".constantize.new(result_object: self, field: field, perform_config: perform_config, skip_rerun: skip_rerun).run
end
result
end
- def run_backend_value(backend, field, value)
+ def run_backend_value(backend, field, value, skip_rerun: false)
perform_config = {
'operator' => backend,
backend => value,
}
- run_backend(field, perform_config)
+ run_backend(field, perform_config, skip_rerun: skip_rerun)
end
def match_workflow(workflow)
diff --git a/app/models/core_workflow/result/backend.rb b/app/models/core_workflow/result/backend.rb
index 4c8452dbf..44567da09 100644
--- a/app/models/core_workflow/result/backend.rb
+++ b/app/models/core_workflow/result/backend.rb
@@ -1,10 +1,11 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class CoreWorkflow::Result::Backend
- def initialize(result_object:, field:, perform_config:)
+ def initialize(result_object:, field:, perform_config:, skip_rerun: false)
@result_object = result_object
@field = field
@perform_config = perform_config
+ @skip_rerun = skip_rerun
end
def field
@@ -12,6 +13,8 @@ class CoreWorkflow::Result::Backend
end
def set_rerun
+ return if @skip_rerun
+
@result_object.rerun = true
end
diff --git a/app/models/core_workflow/result/base_option.rb b/app/models/core_workflow/result/base_option.rb
index 5bf01932b..a18ca4962 100644
--- a/app/models/core_workflow/result/base_option.rb
+++ b/app/models/core_workflow/result/base_option.rb
@@ -2,8 +2,6 @@
class CoreWorkflow::Result::BaseOption < CoreWorkflow::Result::Backend
def remove_excluded_param_values
- return if skip?
-
if @result_object.payload['params'][field].is_a?(Array)
remove_array
elsif excluded_by_restrict_values?(@result_object.payload['params'][field])
@@ -26,7 +24,7 @@ class CoreWorkflow::Result::BaseOption < CoreWorkflow::Result::Backend
end
def remove_string
- @result_object.payload['params'][field] = nil
+ @result_object.payload['params'][field] = @result_object.result[:restrict_values][field]&.first
set_rerun
end
diff --git a/app/models/core_workflow/result/fill_in.rb b/app/models/core_workflow/result/fill_in.rb
index cd9c00da4..8b6a34031 100644
--- a/app/models/core_workflow/result/fill_in.rb
+++ b/app/models/core_workflow/result/fill_in.rb
@@ -11,7 +11,7 @@ class CoreWorkflow::Result::FillIn < CoreWorkflow::Result::Backend
end
def skip?
- return true if fill_in_value.blank?
+ return true if fill_in_value.nil?
return true if params_set?
return true if fill_in_set?
diff --git a/app/models/core_workflow/result/select.rb b/app/models/core_workflow/result/select.rb
index 24ce3e5fc..695151dc9 100644
--- a/app/models/core_workflow/result/select.rb
+++ b/app/models/core_workflow/result/select.rb
@@ -11,7 +11,7 @@ class CoreWorkflow::Result::Select < CoreWorkflow::Result::Backend
end
def skip?
- return true if select_value.blank?
+ return true if select_value.nil?
return true if params_set?
return true if select_set?
diff --git a/app/models/cti/driver/placetel.rb b/app/models/cti/driver/placetel.rb
index 7551bd7b1..e4d2aa8a0 100644
--- a/app/models/cti/driver/placetel.rb
+++ b/app/models/cti/driver/placetel.rb
@@ -88,12 +88,13 @@ class Cti::Driver::Placetel < Cti::Driver::Base
list = Cache.read('placetelGetVoipUsers')
return list if list
- response = UserAgent.post(
- 'https://api.placetel.de/api/getVoIPUsers.json',
- {
- api_key: @config[:api_token],
- },
+ response = UserAgent.get(
+ 'https://api.placetel.de/v2/sip_users',
+ {},
{
+ headers: {
+ Authorization: "Bearer #{@config[:api_token]}",
+ },
log: {
facility: 'placetel',
},
@@ -130,13 +131,9 @@ class Cti::Driver::Placetel < Cti::Driver::Base
list = {}
result.each do |entry|
next if entry['name'].blank?
+ next if entry['sipuid'].blank?
- if entry['uid'].present?
- list[entry['uid']] = entry['name']
- end
- next if entry['uid2'].blank?
-
- list[entry['uid2']] = entry['name']
+ list[entry['sipuid']] = entry['name']
end
Cache.write('placetelGetVoipUsers', list, { expires_in: 24.hours })
list
diff --git a/app/models/data_privacy_task.rb b/app/models/data_privacy_task.rb
index 6733400d1..d28569ccd 100644
--- a/app/models/data_privacy_task.rb
+++ b/app/models/data_privacy_task.rb
@@ -25,19 +25,38 @@ class DataPrivacyTask < ApplicationModel
handle_exception(e)
end
+ # set user inactive before destroy to prevent
+ # new online notifications or other events while
+ # the deletion process is running
+ # https://github.com/zammad/zammad/issues/3942
+ def update_inactive(object)
+ object.update(active: false)
+ end
+
def perform_deletable
- return if deletable.blank?
+ return if !deletable_type.constantize.exists?(id: deletable_id)
prepare_deletion_preview
save!
if delete_organization?
- deletable.organization.destroy(associations: true)
+ perform_organization
else
- deletable.destroy
+ perform_user
end
end
+ def perform_organization
+ update_inactive(deletable.organization)
+ deletable.organization.members.find_each { |user| update_inactive(user) }
+ deletable.organization.destroy(associations: true)
+ end
+
+ def perform_user
+ update_inactive(deletable)
+ deletable.destroy
+ end
+
def handle_exception(e)
Rails.logger.error e
preferences[:error] = "ERROR: #{e.inspect}"
diff --git a/app/models/group.rb b/app/models/group.rb
index a3150d0e3..91b36f94e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -9,7 +9,6 @@ class Group < ApplicationModel
include HasHistory
include HasObjectManagerAttributes
include HasCollectionUpdate
- include HasTicketCreateScreenImpact
include HasSearchIndexBackend
include Group::Assets
diff --git a/app/models/object_manager/attribute.rb b/app/models/object_manager/attribute.rb
index 404b5b3d1..303dbccbd 100644
--- a/app/models/object_manager/attribute.rb
+++ b/app/models/object_manager/attribute.rb
@@ -367,6 +367,12 @@ possible types
return record
end
+ # add maximum position only for new records with blank position
+ if !record && data[:position].blank?
+ maximum_position = where(object_lookup_id: data[:object_lookup_id]).maximum(:position)
+ data[:position] = maximum_position.present? ? maximum_position + 1 : 1
+ end
+
# do not allow to overwrite certain attributes
if !force
data[:editable] = true
@@ -820,7 +826,7 @@ is certain attribute used by triggers, overviews or schedulers
end
record = object_lookup.name.constantize.new
- if record.respond_to?(name.to_sym) && record.attributes.key?(name) && new_record?
+ if new_record? && (record.respond_to?(name.to_sym) || record.attributes.key?(name))
errors.add(:name, "#{name} already exists!")
end
@@ -932,12 +938,7 @@ is certain attribute used by triggers, overviews or schedulers
[{ failed: local_data_option[:future].nil?,
message: 'must have boolean value for :future' },
{ failed: local_data_option[:past].nil?,
- message: 'must have boolean value for :past' },
- { failed: local_data_option[:diff].nil?,
- message: 'must have integer value for :diff (in hours)' }]
- when 'date'
- [{ failed: local_data_option[:diff].nil?,
- message: 'must have integer value for :diff (in days)' }]
+ message: 'must have boolean value for :past' }]
else
[]
end
diff --git a/app/models/object_manager/attribute/set_defaults.rb b/app/models/object_manager/attribute/set_defaults.rb
index e8fb41a78..915033565 100644
--- a/app/models/object_manager/attribute/set_defaults.rb
+++ b/app/models/object_manager/attribute/set_defaults.rb
@@ -15,25 +15,30 @@ class ObjectManager
method_name = "#{attr}="
return if !record.respond_to? method_name
- return if record.send(attr).present?
+ return if record.send("#{attr}_came_from_user?")
record.send method_name, build_value(config)
end
def build_value(config)
- case config[:data_type]
- when 'date'
- config[:diff].days.from_now
- when 'datetime'
- config[:diff].hours.from_now
- else
- config[:default]
- end
+ method_name = "build_value_#{config[:data_type]}"
+
+ return send(method_name, config) if respond_to?(method_name, true)
+
+ config[:default]
+ end
+
+ def build_value_date(config)
+ config[:diff]&.days&.from_now
+ end
+
+ def build_value_datetime(config)
+ config[:diff]&.hours&.from_now&.change(usec: 0, sec: 0)
end
def attributes_for(record)
query = ObjectManager::Attribute.active.editable.for_object(record.class)
- cache_key = "#{query.cache_key}/attribute_defaults"
+ cache_key = "#{query.cache_key_with_version}/attribute_defaults"
Rails.cache.fetch cache_key do
query
diff --git a/app/models/role.rb b/app/models/role.rb
index 7cdde514c..af9279db4 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -8,7 +8,6 @@ class Role < ApplicationModel
include ChecksLatestChangeObserved
include HasGroups
include HasCollectionUpdate
- include HasTicketCreateScreenImpact
include Role::Assets
diff --git a/app/models/sla.rb b/app/models/sla.rb
index 5accf2d84..50225f5ec 100644
--- a/app/models/sla.rb
+++ b/app/models/sla.rb
@@ -13,6 +13,8 @@ class Sla < ApplicationModel
validates :name, presence: true
+ validate :cannot_have_response_and_update
+
store :condition
store :data
@@ -23,7 +25,7 @@ class Sla < ApplicationModel
def self.for_ticket(ticket)
fallback = nil
- all.order(:name, :created_at).find_each do |record|
+ all.order(:name, :created_at).as_batches(size: 10) do |record|
if record.condition.present?
return record if record.condition_matches?(ticket)
else
@@ -32,4 +34,12 @@ class Sla < ApplicationModel
end
fallback
end
+
+ private
+
+ def cannot_have_response_and_update
+ return if response_time.blank? || update_time.blank?
+
+ errors.add :base, 'cannot have both response time and update time'
+ end
end
diff --git a/app/models/smime_certificate.rb b/app/models/smime_certificate.rb
index 932e79632..6d62e968d 100644
--- a/app/models/smime_certificate.rb
+++ b/app/models/smime_certificate.rb
@@ -3,6 +3,28 @@
class SMIMECertificate < ApplicationModel
validates :fingerprint, uniqueness: { case_sensitive: true }
+ def self.parts(raw)
+ raw.scan(%r{-----BEGIN[^-]+-----.+?-----END[^-]+-----}m)
+ end
+
+ def self.create_private_keys(raw, secret)
+ parts(raw).select { |part| part.include?('PRIVATE KEY') }.each do |part|
+ private_key = OpenSSL::PKey.read(part, secret)
+ modulus = private_key.public_key.n.to_s(16)
+ certificate = find_by(modulus: modulus)
+
+ raise Exceptions::UnprocessableEntity, 'Unable for find certificate for this private key.' if !certificate
+
+ certificate.update!(private_key: part, private_key_secret: secret)
+ end
+ end
+
+ def self.create_certificates(raw)
+ parts(raw).select { |part| part.include?('CERTIFICATE') }.each_with_object([]) do |part, result|
+ result << create!(public_key: part)
+ end
+ end
+
def self.parse(raw)
OpenSSL::X509::Certificate.new(raw.gsub(%r{(?:TRUSTED\s)?(CERTIFICATE---)}, '\1'))
end
diff --git a/app/models/ticket/article/adds_metadata_origin_by_id.rb b/app/models/ticket/article/adds_metadata_origin_by_id.rb
index e91ad567e..eb6415f78 100644
--- a/app/models/ticket/article/adds_metadata_origin_by_id.rb
+++ b/app/models/ticket/article/adds_metadata_origin_by_id.rb
@@ -29,6 +29,10 @@ module Ticket::Article::AddsMetadataOriginById
type_name = type.name
return true if type_name != 'phone' && type_name != 'note' && type_name != 'web'
+ organization = created_by.organization
+
+ return true if organization&.shared? && organization.members.include?(ticket.customer)
+
self.origin_by_id = ticket.customer_id
end
end
diff --git a/app/models/transaction/notification.rb b/app/models/transaction/notification.rb
index 3e89d8ef5..03964e8ac 100644
--- a/app/models/transaction/notification.rb
+++ b/app/models/transaction/notification.rb
@@ -75,10 +75,11 @@ class Transaction::Notification
# apply out of office agents
possible_recipients_additions = Set.new
possible_recipients.each do |user|
- recursive_ooo_replacements(
+ ooo_replacements(
user: user,
replacements: possible_recipients_additions,
reasons: recipients_reason,
+ ticket: ticket,
)
end
@@ -339,26 +340,16 @@ class Transaction::Notification
private
- def recursive_ooo_replacements(user:, replacements:, reasons:, level: 0)
- if level == 10
- Rails.logger.warn("Found more than 10 replacement levels for agent #{user}.")
- return
- end
-
+ def ooo_replacements(user:, replacements:, ticket:, reasons:)
replacement = user.out_of_office_agent
+
return if !replacement
- # return for already found, added and checked users
- # to prevent re-doing complete lookup paths
+
+ return if !TicketPolicy.new(replacement, ticket).agent_read_access?
+
return if !replacements.add?(replacement)
reasons[replacement.id] = 'are the out-of-office replacement of the owner'
-
- recursive_ooo_replacements(
- user: replacement,
- replacements: replacements,
- reasons: reasons,
- level: level + 1
- )
end
def possible_recipients_of_group(group_id)
diff --git a/app/models/user.rb b/app/models/user.rb
index 2f01b9c57..eff3483a8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -12,9 +12,7 @@ class User < ApplicationModel
include HasGroups
include HasRoles
include HasObjectManagerAttributes
- include ::HasTicketCreateScreenImpact
include HasTaskbars
- include User::HasTicketCreateScreenImpact
include User::Assets
include User::Avatar
include User::Search
@@ -219,10 +217,15 @@ returns
=end
- def out_of_office_agent(loop_user_ids: [])
+ def out_of_office_agent(loop_user_ids: [], stack_depth: 10)
return if !out_of_office?
return if out_of_office_replacement_id.blank?
+ if stack_depth.zero?
+ Rails.logger.warn("Found more than 10 replacement levels for agent #{self}.")
+ return self
+ end
+
user = User.find_by(id: out_of_office_replacement_id)
# stop if users are occuring multiple times to prevent endless loops
@@ -230,7 +233,7 @@ returns
loop_user_ids |= [out_of_office_replacement_id]
- ooo_agent = user.out_of_office_agent(loop_user_ids: loop_user_ids)
+ ooo_agent = user.out_of_office_agent(loop_user_ids: loop_user_ids, stack_depth: stack_depth - 1)
return ooo_agent if ooo_agent.present?
user
diff --git a/app/models/user/has_ticket_create_screen_impact.rb b/app/models/user/has_ticket_create_screen_impact.rb
deleted file mode 100644
index 07526ee0f..000000000
--- a/app/models/user/has_ticket_create_screen_impact.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
-
-module User::HasTicketCreateScreenImpact
- extend ActiveSupport::Concern
-
- def push_ticket_create_screen?
- return true if destroyed?
- return false if %w[id login firstname lastname preferences active].none? do |attribute|
- saved_change_to_attribute?(attribute)
- end
-
- permissions?('ticket.agent')
- end
-end
diff --git a/app/views/tests/view_helpers.html.erb b/app/views/tests/view_helpers.html.erb
new file mode 100644
index 000000000..080d04cfc
--- /dev/null
+++ b/app/views/tests/view_helpers.html.erb
@@ -0,0 +1,13 @@
+
+<%= javascript_include_tag "/assets/tests/qunit-1.21.0.js", "/assets/tests/view_helpers.js", nonce: true %>
+
+
+
+<%= javascript_tag nonce: true do -%>
+<% end -%>
+
+
diff --git a/coffeelint.json b/coffeelint.json
index 548e8e3e5..ca49e3c31 100644
--- a/coffeelint.json
+++ b/coffeelint.json
@@ -131,5 +131,8 @@
},
"transform_messes_up_line_numbers": {
"level": "error"
+ },
+ "prevent_underscore_backport": {
+ "level": "error"
}
}
diff --git a/config/initializers/active_record_as_batches.rb b/config/initializers/active_record_as_batches.rb
new file mode 100644
index 000000000..c249dfeb8
--- /dev/null
+++ b/config/initializers/active_record_as_batches.rb
@@ -0,0 +1,51 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+# https://github.com/telent/ar-as-batches
+# TODO: Should be reconsidered with rails 6.1 because then
+# find_each might be able to handle order as well
+# e.g. Ticket::Priority.order(updated: :desc).find_each... is not possbile atm with find_each
+module ActiveRecord
+ module AsBatches
+ class Batch
+ def initialize(arel, args)
+ @offset = arel.offset || 0
+ @limit = arel.limit
+ @size = args[:size] || 100
+ return if !@limit || (@limit > @size)
+
+ @size = @limit
+ end
+
+ def get_records(query)
+ query.offset(@offset).limit(@size).all
+ end
+
+ def as_batches(query, &blk)
+ records = get_records(query)
+ while records.any?
+ @offset += records.size
+ records.each(&blk)
+
+ if @limit
+ @limit -= records.size
+ if @limit < size
+ @size = @limit
+ end
+
+ return if @limit.zero?
+ end
+
+ records = get_records(query)
+ end
+ end
+ end
+
+ def as_batches(args = {}, &blk)
+ Batch.new(arel, args).as_batches(self, &blk)
+ end
+ end
+
+ class Relation
+ include AsBatches
+ end
+end
diff --git a/config/initializers/db_preferences.rb b/config/initializers/db_preferences.rb
index b4ade9b1a..265b0d552 100644
--- a/config/initializers/db_preferences.rb
+++ b/config/initializers/db_preferences.rb
@@ -6,6 +6,13 @@ when 'mysql2'
Rails.application.config.db_case_sensitive = false
Rails.application.config.db_like = 'LIKE'
Rails.application.config.db_null_byte = true
+
+ # Because of missing ticket updates in high load environments
+ # we changed the transaction isolation level equally to postgres
+ # to READ COMMITTED which fixed the problem entirely #3877
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.set_callback :checkout, :before do |conn|
+ conn.execute('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED')
+ end
when 'postgresql'
Rails.application.config.db_4bytes_utf8 = true
Rails.application.config.db_case_sensitive = true
diff --git a/config/routes/test.rb b/config/routes/test.rb
index eebd02eac..c0e71e57a 100644
--- a/config/routes/test.rb
+++ b/config/routes/test.rb
@@ -37,6 +37,7 @@ Zammad::Application.routes.draw do
match '/tests_text_module', to: 'tests#text_module', via: :get
match '/tests_color_object', to: 'tests#color_object', via: :get
match '/tests_kb_video_embeding', to: 'tests#kb_video_embeding', via: :get
+ match '/tests_view_helpers', to: 'tests#view_helpers', via: :get
match '/tests/wait/:sec', to: 'tests#wait', via: :get
match '/tests/raised_exception', to: 'tests#error_raised_exception', via: :get
diff --git a/contrib/backup/config.dist b/contrib/backup/config.dist
index 221ca9a8d..72ac733fc 100644
--- a/contrib/backup/config.dist
+++ b/contrib/backup/config.dist
@@ -2,7 +2,10 @@
#
# zammad backup script config
#
+# Learn more about the options below at
+# https://docs.zammad.org/en/latest/appendix/backup-and-restore/configuration.html
BACKUP_DIR='/var/tmp/zammad_backup'
HOLD_DAYS='10'
+FULL_FS_DUMP='yes'
DEBUG='no'
diff --git a/contrib/backup/functions b/contrib/backup/functions
index 18afe81f0..7a3627662 100644
--- a/contrib/backup/functions
+++ b/contrib/backup/functions
@@ -3,6 +3,32 @@
# Zammad backup script functions
#
+function demand_backup_conf () {
+ if [ -f "${BACKUP_SCRIPT_PATH}/config" ]; then
+ # Ensure we're inside of our Backup-Script folder (see issue 2508)
+ # shellcheck disable=SC2164
+ cd "${BACKUP_SCRIPT_PATH}"
+
+ # import config
+ . ${BACKUP_SCRIPT_PATH}/config
+ else
+ echo -e "\n The 'config' file is missing!"
+ echo -e " Please copy ${BACKUP_SCRIPT_PATH}/config.dist to ${BACKUP_SCRIPT_PATH}/config before running $0!\n"
+ echo -e " Learn more about the backup configuration at https://docs.zammad.org/en/latest/appendix/backup-and-restore/configuration.html"
+ exit 1
+ fi
+
+ # Check if filesystem full dump setting exists and fall back if not
+ if [ -z ${FULL_FS_DUMP+x} ]; then
+ # Falling back to old default behavior
+ FULL_FS_DUMP='yes'
+
+ if [ "${DEBUG}" == "yes" ]; then
+ echo "FULL_FS_DUMP is not set, falling back to 'yes' to produce a full backup."
+ fi
+ fi
+}
+
function get_zammad_dir () {
ZAMMAD_DIR="$(echo ${BACKUP_SCRIPT_PATH} | sed -e 's#/contrib/backup.*##g')"
}
@@ -30,6 +56,16 @@ function get_db_credentials () {
DB_USER="$(grep -m 1 '^[[:space:]]*username:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*username:[[:space:]]*//g')"
DB_PASS="$(grep -m 1 '^[[:space:]]*password:' < ${ZAMMAD_DIR}/config/database.yml | sed -e 's/.*password:[[:space:]]*//g')"
+ if [ "${DB_ADAPTER}" == "postgresql" ]; then
+ # Ensure that HOST and PORT are not empty, provide defaults if needed.
+ if [ "${DB_HOST}x" == "x" ]; then
+ DB_HOST="localhost"
+ fi
+ if [ "${DB_PORT}x" == "x" ]; then
+ DB_PORT="5432"
+ fi
+ fi
+
if [ "${DEBUG}" == "yes" ]; then
echo "adapter=${DB_ADAPTER} dbhost=${DB_HOST} dbport=${DB_PORT} dbname=${DB_NAME} dbuser=${DB_USER} dbpass=${DB_PASS}"
fi
@@ -38,14 +74,105 @@ function get_db_credentials () {
function backup_dir_create () {
test -d ${BACKUP_DIR} || mkdir -p ${BACKUP_DIR}
+ state=$?
+
+ if [ "${state}" == "1" ]; then
+ echo -e "\n\n # ERROR(${state}) - Creation of backup directory failed. Please double check permissions."
+ echo -e " #-> BACKUP WAS NOT SUCCESSFUL"
+ exit 3
+ fi
+
if [ "${DEBUG}" == "yes" ]; then
echo "backup dir is ${BACKUP_DIR}"
fi
}
+backup_file_write_test () {
+ # We're testing if we can actually write into the provided directory with
+ # the current user before continuing.
+ touch ${BACKUP_DIR}/write_test 2> /dev/null
+
+ state=$?
+
+ if [ "${state}" == "1" ]; then
+ # We're checking for critical restoration errors
+ # It may not cover all possible errors which is out of scope of this script
+ echo -e "\n\n # ERROR(${state}) - Creation of backup files was not possible. Double check permissions."
+ echo -e " #-> BACKUP WAS NOT SUCCESSFUL"
+ exit 3
+ fi
+
+ rm -f ${BACKUP_DIR}/write_test
+}
+
+backup_file_read_test () {
+ # We're testing if we can read the provided file names before
+ # starting. Other wise handling would be more difficult depending on
+ # the installation type
+
+ if [ "${DEBUG}" == "yes" ]; then
+ echo "I've been looking for these backup files: "
+ echo "- ${BACKUP_DIR}/${RESTORE_FILE_DATE}_zammad_files.tar.gz"
+ echo "- ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz"
+ fi
+
+ if [[ (! -r "${BACKUP_DIR}/${RESTORE_FILE_DATE}_zammad_files.tar.gz") || (! -r "${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz") ]]; then
+ echo -e "\n\n # ERROR - Cannot read on or more of my backup files. Double check permissions."
+ echo -e " #-> RESTORE WAS NOT SUCCESSFUL"
+ exit 3
+ fi
+}
+
+function check_empty_password () {
+ if [ "${DB_PASS}x" == "x" ]; then
+ echo "# ERROR - Found an empty database password ..."
+ echo "# - This may be intended or not, however - this script does not support this."
+ echo "# - If you don't know how to continue, consult https://docs.zammad.org/en/latest/appendix/backup-and-restore/index.html"
+ exit 2
+ fi
+}
+
function backup_files () {
echo "creating file backup..."
- tar -C / -czf ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz --exclude='tmp' ${ZAMMAD_DIR#/}
+
+ if [ "${FULL_FS_DUMP}" == 'yes' ]; then
+ echo " ... as full dump"
+ tar -C / -czf ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz\
+ --exclude='tmp' --exclude='config/database.yml' ${ZAMMAD_DIR#/}
+
+ state=$?
+ else
+ echo " ... only with productive data (attachments)"
+
+ if [ ! -d "${ZAMMAD_DIR}/storage/" ]; then
+ # Admin has requested an attachment backup only, however, there is no storage
+ # directory. We'll warn Mr.Admin and create the directory as workaround.
+ echo " ... WARNING: You don't seem to have any attachments in the file system!"
+ echo " ... Please consult https://docs.zammad.org/en/latest/appendix/backup-and-restore/troubleshooting.html"
+ echo " ... Creating empty storage directory so the backup can continue ..."
+
+ mkdir -p ${ZAMMAD_DIR}/storage/
+ chown -R zammad:zammad ${ZAMMAD_DIR}/storage/
+ fi
+
+ tar -C / -czf ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz\
+ ${ZAMMAD_DIR#/}/storage/\
+
+ state=$?
+ fi
+
+ if [ $state == '2' ]; then
+ echo "# ERROR(2) - File backup reported a fatal error."
+ echo "- Check file permissions and try again."
+ echo -e " \n# BACKUP WAS NOT SUCCESSFUL"
+ exit 1
+ fi
+
+ if [ $state == '1' ]; then
+ echo "# WARNING - Files have changed during backup."
+ echo "- This indicates your Zammad instance is running and thus may be normal."
+ fi
+
ln -sfn ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz ${BACKUP_DIR}/latest_zammad_files.tar.gz
}
@@ -76,12 +203,29 @@ function backup_db () {
--no-privileges --no-owner \
--compress 6 --file "${BACKUP_DIR}/${TIMESTAMP}_zammad_db.psql.gz"
+ state=$?
+
+ if [ "${state}" == "1" ]; then
+ # We're checking for critical restoration errors
+ # It may not cover all possible errors which is out of scope of this script
+ echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid."
+ echo -e " #-> BACKUP WAS NOT SUCCESSFUL"
+ exit 2
+ fi
+
ln -sfn ${BACKUP_DIR}/${TIMESTAMP}_zammad_db.psql.gz ${BACKUP_DIR}/latest_zammad_db.psql.gz
else
- echo "DB ADAPTER not found. if its sqlite backup is already saved in the filebackup"
+ echo -e "\n\n # ERROR - Database type or database.yml incorrect. Unsupported database type found."
+ echo -e " #-> BACKUP WAS NOT SUCCESSFUL"
+ exit 2
fi
}
+function backup_chmod_dump_data () {
+ echo "Ensuring dump permissions ..."
+ chmod 600 ${BACKUP_DIR}/${TIMESTAMP}_zammad_db.psql.gz ${BACKUP_DIR}/${TIMESTAMP}_zammad_files.tar.gz
+}
+
function check_database_config_exists () {
if [ -f ${ZAMMAD_DIR}/config/database.yml ]; then
get_db_credentials
@@ -95,7 +239,9 @@ function restore_warning () {
if [ -n "${1}" ]; then
CHOOSE_RESTORE="yes"
else
- echo -e "The restore will delete your current config and database! \nBe sure to have a backup available! \n"
+ echo -e "The restore will delete your current database! \nBe sure to have a backup available! \n"
+ echo -e "Please ensure to have twice the storage of the uncompressed backup size! \n\n"
+ echo -e "Note that the restoration USUALLY requires root permissions as services are stopped! \n\n"
echo -e "Enter 'yes' if you want to proceed!"
read -p 'Restore?: ' CHOOSE_RESTORE
fi
@@ -106,6 +252,21 @@ function restore_warning () {
fi
}
+function db_helper_warning () {
+ echo -e " # WARNING: THIS SCRIPT CHANGES CREDENTIALS, DO NOT CONTINUE IF YOU DON'T KNOW WHAT YOU'RE DOING! \n\n"
+
+ echo -e " Limitations:"
+ echo -e " - only works for local postgresql installations"
+ echo -e " - only works for postgresql\n"
+ echo -e "Enter 'yes' if you want to proceed!"
+ read -p 'ALTER zammad users password?: ' DB_HELPER
+
+ if [ "${DB_HELPER}" != "yes" ]; then
+ echo "Helper script aborted!"
+ exit 1
+ fi
+}
+
function get_restore_dates () {
RESTORE_FILE_DATES="$(find ${BACKUP_DIR} -type f -iname '*_zammad_files.tar.gz' | sed -e "s#${BACKUP_DIR}/##g" -e "s#_zammad_files.tar.gz##g" | sort)"
@@ -173,6 +334,12 @@ function start_zammad () {
function stop_zammad () {
echo "# Stopping Zammad"
${INIT_CMD} stop zammad
+
+ if [ "$?" != "0" ]; then
+ echo -e "\n\n # WARNING: You don't seem to have administrative permissions!"
+ echo -e " #-> This may be fine if you're on a source code installation."
+ echo -e " #-> Please ensure that Zammad is NOT running - otherwise restore will FAIL.\n"
+ fi
}
function restore_zammad () {
@@ -214,12 +381,39 @@ function restore_zammad () {
create_pgpassfile
- zcat < ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz \
- | psql ${DB_HOST:+--host $DB_HOST} ${DB_PORT:+--port $DB_PORT} ${DB_USER:+--username $DB_USER} --dbname ${DB_NAME}
+ # We're removing uncritical dump information that caused "ugly" error
+ # messages on older script versions. These could safely be ignored.
+ zcat ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz | \
+ sed '/^CREATE EXTENSION IF NOT EXISTS plpgsql/d'| \
+ sed '/^COMMENT ON EXTENSION plpgsql/d'| \
+ psql -q -b -o /dev/null \
+ ${DB_HOST:+--host $DB_HOST} ${DB_PORT:+--port $DB_PORT} ${DB_USER:+--username $DB_USER} --dbname ${DB_NAME}
+
+ state=$?
+
+ if [[ ("${state}" == "1") || ( "${state}" == "2") || ( "${state}" == "3") ]]; then
+ # We're checking for critical restoration errors
+ # It may not cover all possible errors which is out of scope of this script
+ echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid."
+ echo -e " #-> RESTORE WAS NOT SUCCESSFUL"
+ exit 2
+ fi
elif [ "${DB_ADAPTER}" == "mysql2" ]; then
echo "# Restoring MySQL DB"
zcat < ${BACKUP_DIR}/${RESTORE_DB_DATE}_zammad_db.${DB_FILE_EXT}.gz | mysql -u${DB_USER} -p${DB_PASS} ${DB_NAME}
+
+ state=$?
+
+ if [ "${state}" != "0" ]; then
+ echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid."
+ echo -e " #-> RESTORE WAS NOT SUCCESSFUL"
+ exit 2
+ fi
+ else
+ echo -e "\n\n # ERROR - Database type or database.yml incorrect. Unsupported database type found."
+ echo -e " #-> RESTORE WAS NOT SUCCESSFUL"
+ exit 2
fi
if command -v zammad > /dev/null; then
@@ -239,16 +433,91 @@ function restore_zammad () {
function restore_files () {
echo "# Restoring Files"
tar -C / --overwrite -xzf ${BACKUP_DIR}/${RESTORE_FILE_DATE}_zammad_files.tar.gz
- echo "# Ensuring correct file rights ..."
+
+ state=$?
+
+ if [[ ($state == '1') || ($state == '2') ]]; then
+ echo "# ERROR(${state}) - File restore reported an error."
+ echo "- Check file permissions, and ensure Zammad IS NOT running, and try again."
+ echo -e " \n# RESTORE WAS NOT SUCCESSFUL"
+ exit 1
+ fi
+
+ echo "# Ensuring correct file permissions ..."
chown -R zammad:zammad ${ZAMMAD_DIR}
}
+function kind_exit () {
+ # We're nice to our admin and bring Zammad back up before exiting
+ start_zammad
+ exit 1
+}
+
+function db_helper_alter_user () {
+ # Get DB credentials
+ get_db_credentials
+
+ if [ "${DB_PASS}x" == "x" ]; then
+ echo "# Found an empty password - I'll be fixing this for you ..."
+
+ DB_PASS="$(tr -dc A-Za-z0-9 < /dev/urandom | head -c10)"
+
+ sed -e "s/.*adapter:.*/ adapter: ${DB_ADAPTER}/" \
+ -e "s/.*username:.*/ username: ${DB_USER}/" \
+ -e "s/.*password:.*/ password: ${DB_PASS}/" \
+ -e "s/.*database:.*/ database: ${DB_NAME}/" < ${ZAMMAD_DIR}/contrib/packager.io/database.yml.pkgr > ${ZAMMAD_DIR}/config/database.yml
+
+ echo "# ... Fixing permission database.yml"
+ chown zammad:zammad ${ZAMMAD_DIR}/config/database.yml
+ fi
+
+ if [ "${DB_USER}x" == "x" ]; then
+
+ echo "ERROR - Your configuration file does not seem to contain a username."
+ echo "Aborting the script - double check your installation."
+
+ kind_exit
+ fi
+
+ if [ "${DB_ADAPTER}" == "postgresql" ]; then
+
+ if [[ ("${DB_HOST}" == "localhost") || "${DB_HOST}" == "127.0.0.1" || "${DB_HOST}" == "::1" ]]; then
+ # Looks like a local pgsql installation - let's continue
+ su -c "psql -c \"ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';\"" postgres
+ state=$?
+
+ else
+ echo "You don't seem to be using a local PostgreSQL installation."
+ echo "This script does not support your installation. No changes were done."
+
+ kind_exit
+ fi
+
+ else
+ echo "You don't seem to use PostgreSQL. This script does not support your installation."
+ echo "No changes were done."
+
+ kind_exit
+ fi
+
+ if [ "${state}" != "0" ]; then
+ echo "ERROR - Our previous command returned an unhandled error code."
+ echo "Check above command output. Please consult https://community.zammad.org if you can't solve this issue on your own."
+
+ kind_exit
+ fi
+}
+
function start_backup_message () {
echo -e "\n# Zammad backup started - $(date)!\n"
}
function start_restore_message () {
- echo -e "\n# Zammad restored started - $(date)!\n"
+ echo -e "\n# Zammad restore started - $(date)!\n"
+}
+
+function start_helper_message () {
+ echo -e "\n # This helper script sets the current Zammad user password on your postgresql server."
}
function finished_backup_message () {
diff --git a/contrib/backup/zammad_backup.sh b/contrib/backup/zammad_backup.sh
index dd2ce68b1..31c34791b 100755
--- a/contrib/backup/zammad_backup.sh
+++ b/contrib/backup/zammad_backup.sh
@@ -6,22 +6,12 @@
# shellcheck disable=SC2046
BACKUP_SCRIPT_PATH="$(dirname $(realpath $0))"
-if [ -f "${BACKUP_SCRIPT_PATH}/config" ]; then
- # Ensure we're inside of our Backup-Script folder (see issue 2508)
- # shellcheck disable=SC2164
- cd "${BACKUP_SCRIPT_PATH}"
-
- # import config
- . ${BACKUP_SCRIPT_PATH}/config
-else
- echo -e "\n The 'config' file is missing!"
- echo -e " Please copy ${BACKUP_SCRIPT_PATH}/config.dist to ${BACKUP_SCRIPT_PATH}/config before running $0!\n"
- exit 1
-fi
-
# import functions
. ${BACKUP_SCRIPT_PATH}/functions
+# ensure we have all options
+demand_backup_conf
+
# exec backup
start_backup_message
@@ -29,14 +19,20 @@ get_zammad_dir
check_database_config_exists
-delete_old_backups
+check_empty_password
get_backup_date
backup_dir_create
+backup_file_write_test
+
+delete_old_backups
+
backup_files
backup_db
+backup_chmod_dump_data
+
finished_backup_message
diff --git a/contrib/backup/zammad_db_user_helper.sh b/contrib/backup/zammad_db_user_helper.sh
new file mode 100644
index 000000000..f5a94fdcf
--- /dev/null
+++ b/contrib/backup/zammad_db_user_helper.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+#
+# This little helper script
+
+# shellcheck disable=SC2046
+BACKUP_SCRIPT_PATH="$(dirname $(realpath $0))"
+
+# import functions
+. ${BACKUP_SCRIPT_PATH}/functions
+
+# exec backup
+start_helper_message
+
+get_zammad_dir
+
+db_helper_warning
+
+check_database_config_exists
+
+detect_initcmd
+
+stop_zammad
+
+db_helper_alter_user
+
+start_zammad
diff --git a/contrib/backup/zammad_restore.sh b/contrib/backup/zammad_restore.sh
index 5a327fa43..9048fa921 100755
--- a/contrib/backup/zammad_restore.sh
+++ b/contrib/backup/zammad_restore.sh
@@ -6,18 +6,12 @@
# shellcheck disable=SC2046
BACKUP_SCRIPT_PATH="$(dirname $(realpath $0))"
-if [ -f "${BACKUP_SCRIPT_PATH}/config" ]; then
- # import config
- . ${BACKUP_SCRIPT_PATH}/config
-else
- echo -e "\n The 'config' file is missing!"
- echo -e " Please copy ${BACKUP_SCRIPT_PATH}/config.dist to ${BACKUP_SCRIPT_PATH}/config before running $0!\n"
- exit 1
-fi
-
# import functions
. ${BACKUP_SCRIPT_PATH}/functions
+# ensure we have all options
+demand_backup_conf
+
# exec restore
start_restore_message
@@ -27,10 +21,14 @@ restore_warning "${1}"
check_database_config_exists
+check_empty_password
+
get_restore_dates
choose_restore_date "${1}"
+backup_file_read_test
+
detect_initcmd
stop_zammad
diff --git a/contrib/packager.io/functions b/contrib/packager.io/functions
index 8f3f91983..d770c4d95 100644
--- a/contrib/packager.io/functions
+++ b/contrib/packager.io/functions
@@ -181,6 +181,9 @@ function update_database_yml () {
-e "s/.*username:.*/ username: ${DB_USER}/" \
-e "s/.*password:.*/ password: ${DB_PASS}/" \
-e "s/.*database:.*/ database: ${DB}/" < ${ZAMMAD_DIR}/contrib/packager.io/database.yml.pkgr > ${ZAMMAD_DIR}/config/database.yml
+
+ echo "# ... Fixing permission database.yml"
+ chown zammad:zammad ${ZAMMAD_DIR}/config/database.yml
}
function initialise_database () {
@@ -301,6 +304,9 @@ function update_or_install () {
setup_elasticsearch
elasticsearch_searchindex_rebuild
+
+ echo "# Enforcing 0600 on database.yml ..."
+ chmod 600 ${ZAMMAD_DIR}/config/database.yml
}
function set_env_vars () {
diff --git a/db/migrate/20120101000001_create_base.rb b/db/migrate/20120101000001_create_base.rb
index 2ce9b29f2..d661ee20d 100644
--- a/db/migrate/20120101000001_create_base.rb
+++ b/db/migrate/20120101000001_create_base.rb
@@ -178,6 +178,7 @@ class CreateBase < ActiveRecord::Migration[4.2]
add_index :groups_users, [:user_id]
add_index :groups_users, [:group_id]
add_index :groups_users, [:access]
+ add_index :groups_users, %i[user_id group_id access]
add_foreign_key :groups_users, :users
add_foreign_key :groups_users, :groups
diff --git a/db/migrate/20120101000010_create_ticket.rb b/db/migrate/20120101000010_create_ticket.rb
index 4d13807d5..5d5d5666f 100644
--- a/db/migrate/20120101000010_create_ticket.rb
+++ b/db/migrate/20120101000010_create_ticket.rb
@@ -446,6 +446,7 @@ class CreateTicket < ActiveRecord::Migration[4.2]
t.references :calendar, null: false
t.column :name, :string, limit: 150, null: true
t.column :first_response_time, :integer, null: true
+ t.column :response_time, :integer, null: true
t.column :update_time, :integer, null: true
t.column :solution_time, :integer, null: true
t.column :condition, :text, limit: 500.kilobytes + 1, null: true
diff --git a/db/migrate/20210614063039_sla_add_response_time.rb b/db/migrate/20210614063039_sla_add_response_time.rb
new file mode 100644
index 000000000..ce609088d
--- /dev/null
+++ b/db/migrate/20210614063039_sla_add_response_time.rb
@@ -0,0 +1,13 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class SlaAddResponseTime < ActiveRecord::Migration[5.2]
+ def change
+ return if !Setting.exists?(name: 'system_init_done')
+
+ change_table :slas do |t|
+ t.integer :response_time
+ end
+
+ Sla.reset_column_information
+ end
+end
diff --git a/db/migrate/20211020131134_issue_3810_custom_date_attribute_no_default.rb b/db/migrate/20211020131134_issue_3810_custom_date_attribute_no_default.rb
new file mode 100644
index 000000000..edde66611
--- /dev/null
+++ b/db/migrate/20211020131134_issue_3810_custom_date_attribute_no_default.rb
@@ -0,0 +1,18 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class Issue3810CustomDateAttributeNoDefault < ActiveRecord::Migration[6.0]
+ def up
+ return if !Setting.exists?(name: 'system_init_done')
+
+ ObjectManager::Attribute
+ .where(data_type: %i[date datetime])
+ .each { |elem| update_single(elem) }
+ end
+
+ def update_single(elem)
+ elem.data_option[:diff] = nil
+ elem.save!
+ rescue => e
+ Rails.logger.error e
+ end
+end
diff --git a/db/migrate/20211026000001_object_manager_ticket_object_update.rb b/db/migrate/20211026000001_object_manager_ticket_object_update.rb
new file mode 100644
index 000000000..93e26d7a9
--- /dev/null
+++ b/db/migrate/20211026000001_object_manager_ticket_object_update.rb
@@ -0,0 +1,94 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class ObjectManagerTicketObjectUpdate < ActiveRecord::Migration[4.2]
+ def up
+
+ # return if it's a new setup
+ return if !Setting.exists?(name: 'system_init_done')
+
+ UserInfo.current_user_id = 1
+ ObjectManager::Attribute.add(
+ force: true,
+ object: 'Ticket',
+ name: 'number',
+ display: '#',
+ data_type: 'input',
+ data_option: {
+ type: 'text',
+ readonly: 1,
+ null: true,
+ maxlength: 60,
+ width: '68px',
+ },
+ editable: false,
+ active: true,
+ screens: {
+ create_top: {},
+ edit: {},
+ },
+ to_create: false,
+ to_migrate: false,
+ to_delete: false,
+ position: 5,
+ )
+
+ ObjectManager::Attribute.add(
+ force: true,
+ object: 'Ticket',
+ name: 'title',
+ display: 'Title',
+ data_type: 'input',
+ data_option: {
+ type: 'text',
+ maxlength: 200,
+ null: false,
+ translate: false,
+ },
+ editable: false,
+ active: true,
+ screens: {
+ create_top: {
+ '-all-' => {
+ null: false,
+ },
+ },
+ edit: {},
+ },
+ to_create: false,
+ to_migrate: false,
+ to_delete: false,
+ position: 8,
+ )
+
+ ObjectManager::Attribute.add(
+ force: true,
+ object: 'Ticket',
+ name: 'organization_id',
+ display: 'Organization',
+ data_type: 'autocompletion_ajax',
+ data_option: {
+ relation: 'Organization',
+ autocapitalize: false,
+ multiple: false,
+ null: true,
+ translate: false,
+ permission: ['ticket.agent'],
+ readonly: 1,
+ },
+ editable: false,
+ active: true,
+ screens: {
+ create_top: {
+ '-all-' => {
+ null: false,
+ },
+ },
+ edit: {},
+ },
+ to_create: false,
+ to_migrate: false,
+ to_delete: false,
+ position: 12,
+ )
+ end
+end
diff --git a/db/migrate/20211028072158_maintenance_remove_active_ldap_sessions.rb b/db/migrate/20211028072158_maintenance_remove_active_ldap_sessions.rb
new file mode 100644
index 000000000..b88b561d9
--- /dev/null
+++ b/db/migrate/20211028072158_maintenance_remove_active_ldap_sessions.rb
@@ -0,0 +1,12 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class MaintenanceRemoveActiveLdapSessions < ActiveRecord::Migration[6.0]
+ def change
+ return if !Setting.exists?(name: 'system_init_done')
+
+ # Only relevant for when ldap integration is used.
+ return if !Setting.get('ldap_integration')
+
+ ActiveRecord::SessionStore::Session.destroy_all
+ end
+end
diff --git a/db/migrate/20211115135421_issue3851.rb b/db/migrate/20211115135421_issue3851.rb
new file mode 100644
index 000000000..458882c2a
--- /dev/null
+++ b/db/migrate/20211115135421_issue3851.rb
@@ -0,0 +1,25 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class Issue3851 < ActiveRecord::Migration[6.0]
+ def change
+ return if !Setting.exists?(name: 'system_init_done')
+
+ fix_follow_up_assignment
+ fix_follow_up_possible
+ end
+
+ def fix_follow_up_assignment
+ follow_up_assignment = ObjectManager::Attribute.for_object('Group').find_by(name: 'follow_up_assignment')
+ follow_up_assignment.data_option['default'] = 'true'
+ follow_up_assignment.screens['create']['-all-']['null'] = false
+ follow_up_assignment.screens['edit']['-all-']['null'] = false
+ follow_up_assignment.save!
+ end
+
+ def fix_follow_up_possible
+ follow_up_possible = ObjectManager::Attribute.for_object('Group').find_by(name: 'follow_up_possible')
+ follow_up_possible.screens['create']['-all-']['null'] = false
+ follow_up_possible.screens['edit']['-all-']['null'] = false
+ follow_up_possible.save!
+ end
+end
diff --git a/db/migrate/20211123144240_issue3622_add_callback_url.rb b/db/migrate/20211123144240_issue3622_add_callback_url.rb
new file mode 100644
index 000000000..29358d9f1
--- /dev/null
+++ b/db/migrate/20211123144240_issue3622_add_callback_url.rb
@@ -0,0 +1,30 @@
+# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+
+class Issue3622AddCallbackUrl < ActiveRecord::Migration[6.0]
+ def change
+ return if !Setting.exists?(name: 'system_init_done')
+
+ configs = {
+ auth_twitter_credentials: 'auth_twitter',
+ auth_facebook_credentials: 'auth_facebook',
+ auth_google_oauth2_credentials: 'auth_google_oauth2',
+ auth_linkedin_credentials: 'auth_linkedin',
+ auth_github_credentials: 'auth_github',
+ auth_gitlab_credentials: 'auth_gitlab',
+ auth_microsoft_office365_credentials: 'auth_microsoft_office365',
+ auth_weibo_credentials: 'auth_weibo',
+ auth_saml_credentials: 'auth_saml',
+ }
+ configs.each do |key, value|
+ config = Setting.find_by(name: key)
+ config.options['form'] << {
+ 'display' => 'Your callback URL',
+ 'null' => true,
+ 'name' => 'callback_url',
+ 'tag' => 'auth_provider',
+ 'provider' => value
+ }
+ config.save!
+ end
+ end
+end
diff --git a/db/migrate/20220131135531_issue3940_add_index.rb b/db/migrate/20220131135531_issue3940_add_index.rb
new file mode 100644
index 000000000..aa5ec184f
--- /dev/null
+++ b/db/migrate/20220131135531_issue3940_add_index.rb
@@ -0,0 +1,10 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+class Issue3940AddIndex < ActiveRecord::Migration[6.0]
+ def change
+ # return if it's a new setup
+ return if !Setting.exists?(name: 'system_init_done')
+
+ add_index :groups_users, %i[user_id group_id access]
+ end
+end
diff --git a/db/seeds/object_manager_attributes.rb b/db/seeds/object_manager_attributes.rb
index af7971427..54aff1f21 100644
--- a/db/seeds/object_manager_attributes.rb
+++ b/db/seeds/object_manager_attributes.rb
@@ -1,5 +1,30 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
+ObjectManager::Attribute.add(
+ force: true,
+ object: 'Ticket',
+ name: 'number',
+ display: '#',
+ data_type: 'input',
+ data_option: {
+ type: 'text',
+ readonly: 1,
+ null: true,
+ maxlength: 60,
+ width: '68px',
+ },
+ editable: false,
+ active: true,
+ screens: {
+ create_top: {},
+ edit: {},
+ },
+ to_create: false,
+ to_migrate: false,
+ to_delete: false,
+ position: 5,
+)
+
ObjectManager::Attribute.add(
force: true,
object: 'Ticket',
@@ -25,7 +50,7 @@ ObjectManager::Attribute.add(
to_create: false,
to_migrate: false,
to_delete: false,
- position: 15,
+ position: 8,
)
ObjectManager::Attribute.add(
@@ -61,6 +86,38 @@ ObjectManager::Attribute.add(
to_delete: false,
position: 10,
)
+
+ObjectManager::Attribute.add(
+ force: true,
+ object: 'Ticket',
+ name: 'organization_id',
+ display: 'Organization',
+ data_type: 'autocompletion_ajax',
+ data_option: {
+ relation: 'Organization',
+ autocapitalize: false,
+ multiple: false,
+ null: true,
+ translate: false,
+ permission: ['ticket.agent'],
+ readonly: 1,
+ },
+ editable: false,
+ active: true,
+ screens: {
+ create_top: {
+ '-all-' => {
+ null: false,
+ },
+ },
+ edit: {},
+ },
+ to_create: false,
+ to_migrate: false,
+ to_delete: false,
+ position: 12,
+)
+
ObjectManager::Attribute.add(
force: true,
object: 'Ticket',
@@ -1634,12 +1691,12 @@ ObjectManager::Attribute.add(
screens: {
create: {
'-all-' => {
- null: true,
+ null: false,
},
},
edit: {
'-all-' => {
- null: true,
+ null: false,
},
},
},
@@ -1656,7 +1713,7 @@ ObjectManager::Attribute.add(
display: 'Assign Follow-Ups',
data_type: 'select',
data_option: {
- default: 'yes',
+ default: 'true',
options: {
true: 'yes',
false: 'no',
@@ -1670,12 +1727,12 @@ ObjectManager::Attribute.add(
screens: {
create: {
'-all-' => {
- null: true,
+ null: false,
},
},
edit: {
'-all-' => {
- null: true,
+ null: false,
},
},
},
diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb
index 6704464d5..d6ee0199a 100644
--- a/db/seeds/settings.rb
+++ b/db/seeds/settings.rb
@@ -1286,6 +1286,13 @@ Setting.create_if_not_exists(
name: 'secret',
tag: 'input',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_twitter',
+ },
],
},
state: {},
@@ -1343,6 +1350,13 @@ Setting.create_if_not_exists(
name: 'app_secret',
tag: 'input',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_facebook',
+ },
],
},
state: {},
@@ -1400,6 +1414,13 @@ Setting.create_if_not_exists(
name: 'client_secret',
tag: 'input',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_google_oauth2',
+ },
],
},
state: {},
@@ -1457,6 +1478,13 @@ Setting.create_if_not_exists(
name: 'app_secret',
tag: 'input',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_linkedin',
+ },
],
},
state: {},
@@ -1514,6 +1542,13 @@ Setting.create_if_not_exists(
name: 'app_secret',
tag: 'input',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_github',
+ },
],
},
state: {},
@@ -1578,6 +1613,13 @@ Setting.create_if_not_exists(
tag: 'input',
placeholder: 'https://gitlab.YOURDOMAIN.com/api/v4/',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_gitlab',
+ },
],
},
state: {},
@@ -1642,6 +1684,13 @@ Setting.create_if_not_exists(
tag: 'input',
placeholder: 'common',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_microsoft_office365',
+ },
],
},
state: {},
@@ -1698,6 +1747,13 @@ Setting.create_if_not_exists(
name: 'client_secret',
tag: 'input',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_weibo',
+ },
],
},
state: {},
@@ -1770,6 +1826,13 @@ Setting.create_if_not_exists(
tag: 'input',
placeholder: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
},
+ {
+ display: 'Your callback URL',
+ null: true,
+ name: 'callback_url',
+ tag: 'auth_provider',
+ provider: 'auth_saml',
+ },
],
},
state: {},
diff --git a/lib/active_support/cache/zammad_file_store.rb b/lib/active_support/cache/zammad_file_store.rb
index ee8abd442..1311ad3ba 100644
--- a/lib/active_support/cache/zammad_file_store.rb
+++ b/lib/active_support/cache/zammad_file_store.rb
@@ -7,6 +7,9 @@ module ActiveSupport
# in certain cases, caches are deleted by other thread at same
# time, just log it
super
+ rescue Errno::ENOENT => e
+ Rails.logger.debug { "Can't write cache (probably related to high load / https://github.com/zammad/zammad/issues/3685) #{name}: #{e.inspect}" }
+ Rails.logger.debug e
rescue => e
Rails.logger.error "Can't write cache #{name}: #{e.inspect}"
Rails.logger.error e
diff --git a/lib/auth/backend/base.rb b/lib/auth/backend/base.rb
index ff3c584c1..347dfa75f 100644
--- a/lib/auth/backend/base.rb
+++ b/lib/auth/backend/base.rb
@@ -21,6 +21,7 @@ class Auth
end
def valid?
+ return false if password.blank? && password_required?
return false if !perform?
authenticated?
@@ -28,6 +29,10 @@ class Auth
private
+ def password_required?
+ true
+ end
+
def perform?
raise NotImplementedError
end
diff --git a/lib/auth/backend/developer.rb b/lib/auth/backend/developer.rb
index 4c63b5a83..4f6637fad 100644
--- a/lib/auth/backend/developer.rb
+++ b/lib/auth/backend/developer.rb
@@ -20,6 +20,13 @@ class Auth
false
end
+ # No password required for developer mode and test environment.
+ #
+ # @returns [Boolean] false
+ def password_required?
+ false
+ end
+
# Overwrites the default behaviour to check for a allowed environment.
#
# @returns [Boolean] true if the environment is development or test.
diff --git a/lib/auth/backend/internal.rb b/lib/auth/backend/internal.rb
index 459c45c13..e398a1be0 100644
--- a/lib/auth/backend/internal.rb
+++ b/lib/auth/backend/internal.rb
@@ -21,7 +21,6 @@ class Auth
#
# @returns [Boolean] true if a internal password for the user is present.
def perform?
- return false if password.blank?
return false if !user.verified && user.source == 'signup'
user.password.present?
diff --git a/lib/escalation.rb b/lib/escalation.rb
index 817c1a017..a41d3a8f3 100644
--- a/lib/escalation.rb
+++ b/lib/escalation.rb
@@ -97,7 +97,7 @@ class Escalation
end
def update_escalations
- ticket.assign_attributes [escalation_first_response, escalation_update, escalation_close]
+ ticket.assign_attributes [escalation_first_response, escalation_response, escalation_update, escalation_close]
.compact
.each_with_object({}) { |elem, memo| memo.merge!(elem) }
@@ -105,7 +105,7 @@ class Escalation
end
def update_statistics
- ticket.assign_attributes [statistics_first_response, statistics_update, statistics_close]
+ ticket.assign_attributes [statistics_first_response, statistics_response, statistics_update, statistics_close]
.compact
.each_with_object({}) { |elem, memo| memo.merge!(elem) }
end
@@ -130,11 +130,41 @@ class Escalation
}
end
- def escalation_update
+ def escalation_update_reset
+ return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
+ return if sla.response_time.present? || sla.update_time.present?
+
+ { update_escalation_at: nil }
+ end
+
+ def escalation_response_timestamp
+ return if escalation_disabled? || ticket.agent_responded?
+
+ ticket.last_contact_customer_at
+ end
+
+ def escalation_response
+ return if sla.response_time.nil?
return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
- nullify = escalation_disabled? || ticket.agent_responded?
- timestamp = nullify ? nil : ticket.last_contact_customer_at
+ timestamp = escalation_response_timestamp
+
+ {
+ update_escalation_at: timestamp ? calculate_time(timestamp, sla.response_time) : nil
+ }
+ end
+
+ def escalation_update_timestamp
+ return if escalation_disabled?
+
+ ticket.last_contact_agent_at || ticket.created_at
+ end
+
+ def escalation_update
+ return if sla.update_time.nil?
+ return if skip_escalation? && !preferences.last_update_at_changed?(ticket)
+
+ timestamp = escalation_update_timestamp
{
update_escalation_at: timestamp ? calculate_time(timestamp, sla.update_time) : nil
@@ -186,11 +216,39 @@ class Escalation
}
end
+ def skip_statistics_response?
+ return true if !forced? && !preferences.last_update_at_changed?(ticket)
+ return true if !sla.response_time
+
+ !ticket.agent_responded?
+ end
+
+ # ATTENTION: Recalculation after SLA change won't happen
+ # SLA change will cause wrong statistics in some edge cases.
+ # Since this changes `update_in_min` calculation to retain longest timespan.
+ # But it does not keep track of previous update times.
+ def statistics_response_applicable?(minutes)
+ ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
+ end
+
+ def statistics_response
+ return if skip_statistics_response?
+
+ minutes = calculate_minutes(ticket.last_contact_customer_at, ticket.last_contact_agent_at)
+
+ return if !forced? && !statistics_response_applicable?(minutes)
+
+ {
+ update_in_min: minutes,
+ update_diff_in_min: minutes ? (sla.response_time - minutes) : nil
+ }
+ end
+
def skip_statistics_update?
return true if !forced? && !preferences.last_update_at_changed?(ticket)
return true if !sla.update_time
- !ticket.agent_responded?
+ ticket.last_contact_agent_at.blank?
end
# ATTENTION: Recalculation after SLA change won't happen
@@ -201,10 +259,28 @@ class Escalation
ticket.update_in_min.blank? || minutes > ticket.update_in_min # keep longest timespan
end
+ def statistics_update_responses
+ ticket
+ .articles
+ .reverse
+ .lazy
+ .select { |article| article.sender&.name == 'Agent' && article.type&.communication }
+ .first(2)
+ end
+
+ def statistics_update_minutes
+ last_agent_responses = statistics_update_responses
+
+ from = last_agent_responses.second&.created_at || ticket.created_at
+ to = last_agent_responses.first&.created_at
+
+ calculate_minutes(from, to)
+ end
+
def statistics_update
return if skip_statistics_update?
- minutes = calculate_minutes(ticket.last_contact_customer_at, ticket.last_contact_agent_at)
+ minutes = statistics_update_minutes
return if !forced? && !statistics_update_applicable?(minutes)
diff --git a/lib/external_credential/google.rb b/lib/external_credential/google.rb
index 10de9ec88..b97207a7c 100644
--- a/lib/external_credential/google.rb
+++ b/lib/external_credential/google.rb
@@ -85,10 +85,9 @@ class ExternalCredential::Google
migrate_channel = nil
Channel.where(area: 'Email::Account').find_each do |channel|
- next if channel.options.dig(:inbound, :options, :user) != user_data[:email]
- next if channel.options.dig(:inbound, :options, :host) != 'imap.gmail.com'
- next if channel.options.dig(:outbound, :options, :user) != user_data[:email]
- next if channel.options.dig(:outbound, :options, :host) != 'smtp.gmail.com'
+ next if channel.options.dig(:inbound, :options, :host)&.downcase != 'imap.gmail.com'
+ next if channel.options.dig(:outbound, :options, :host)&.downcase != 'smtp.gmail.com'
+ next if channel.options.dig(:outbound, :options, :user)&.downcase != user_data[:email].downcase && channel.options.dig(:outbound, :email)&.downcase != user_data[:email].downcase
migrate_channel = channel
diff --git a/lib/external_credential/microsoft365.rb b/lib/external_credential/microsoft365.rb
index ccf006341..922583c3b 100644
--- a/lib/external_credential/microsoft365.rb
+++ b/lib/external_credential/microsoft365.rb
@@ -89,10 +89,9 @@ class ExternalCredential::Microsoft365
migrate_channel = nil
Channel.where(area: 'Email::Account').find_each do |channel|
- next if channel.options.dig(:inbound, :options, :user) != user_data[:email]
- next if channel.options.dig(:inbound, :options, :host) != 'outlook.office365.com'
- next if channel.options.dig(:outbound, :options, :user) != user_data[:email]
- next if channel.options.dig(:outbound, :options, :host) != 'smtp.office365.com'
+ next if channel.options.dig(:inbound, :options, :host)&.downcase != 'outlook.office365.com'
+ next if channel.options.dig(:outbound, :options, :host)&.downcase != 'smtp.office365.com'
+ next if channel.options.dig(:outbound, :options, :user)&.downcase != user_data[:email].downcase && channel.options.dig(:outbound, :email)&.downcase != user_data[:email].downcase
migrate_channel = channel
diff --git a/lib/gitlab/http_client.rb b/lib/gitlab/http_client.rb
index 76338e204..f8493dbdc 100644
--- a/lib/gitlab/http_client.rb
+++ b/lib/gitlab/http_client.rb
@@ -14,6 +14,19 @@ class GitLab
@endpoint = endpoint
end
+ # returns path of the subfolder of the endpoint if exists
+ def endpoint_path
+ path = URI.parse(endpoint).path
+ return if path.blank?
+ return if path == '/api/graphql'
+
+ if path.start_with?('/')
+ path = path[1..]
+ end
+
+ path.sub('api/graphql', '')
+ end
+
def perform(payload)
response = UserAgent.post(
endpoint,
diff --git a/lib/gitlab/linked_issue.rb b/lib/gitlab/linked_issue.rb
index ae9ff8d9e..e05829e0e 100644
--- a/lib/gitlab/linked_issue.rb
+++ b/lib/gitlab/linked_issue.rb
@@ -106,6 +106,10 @@ class GitLab
fullpath = $2
id = $3
+ if client.endpoint_path.present?
+ fullpath.sub!(client.endpoint_path, '')
+ end
+
if client.endpoint.exclude?(host)
raise Exceptions::UnprocessableEntity, "Issue link doesn't match configured GitLab endpoint '#{client.endpoint}'"
end
diff --git a/lib/html_sanitizer.rb b/lib/html_sanitizer.rb
index 2ca0819b9..d7226119e 100644
--- a/lib/html_sanitizer.rb
+++ b/lib/html_sanitizer.rb
@@ -7,7 +7,7 @@ class HtmlSanitizer
=begin
-satinize html string based on whiltelist
+sanitize html string based on whiltelist
string = HtmlSanitizer.strict(string, external)
@@ -409,7 +409,7 @@ cleanup html string:
=begin
-reolace inline images with cid images
+replace inline images with cid images
string = HtmlSanitizer.replace_inline_images(article.body)
@@ -450,7 +450,7 @@ reolace inline images with cid images
=begin
-satinize style of img tags
+sanitize style of img tags
string = HtmlSanitizer.dynamic_image_size(article.body)
diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb
index be4af611b..30f68cde9 100644
--- a/lib/search_index_backend.rb
+++ b/lib/search_index_backend.rb
@@ -438,6 +438,8 @@ example for aggregations within one year
data = selector2query(selectors, options, aggs_interval)
+ verify_date_range(url, data)
+
response = make_request(url, data: data)
if !response.success?
@@ -1208,4 +1210,87 @@ helper method for making HTTP calls and raising error if response was not succes
)
end
+ # verifies date range ElasticSearch payload
+ #
+ # @param url [String] of ElasticSearch
+ # @param payload [Hash] Elasticsearch query payload
+ #
+ # @return [Boolean] or raises error
+ def self.verify_date_range(url, payload)
+ ranges_payload = payload.dig(:query, :bool, :must)
+
+ return true if ranges_payload.nil?
+
+ ranges = ranges_payload
+ .select { |elem| elem.key? :range }
+ .map { |elem| [elem[:range].keys.first, convert_es_date_range(elem)] }
+ .each_with_object({}) { |elem, sum| (sum[elem.first] ||= []) << elem.last }
+
+ return true if ranges.all? { |_, ranges_by_key| verify_single_key_range(ranges_by_key) }
+
+ error_prefix = "Unable to process request to elasticsearch URL '#{url}'."
+ error_suffix = "Payload:\n#{payload.to_json}"
+ error_message = 'Conflicting date ranges'
+
+ result = "#{error_prefix} #{error_message} #{error_suffix}"
+ Rails.logger.error result.first(40_000)
+
+ raise result
+ end
+
+ # checks if all ranges are overlaping
+ #
+ # @param ranges [Array>] to use in search
+ #
+ # @return [Boolean]
+ def self.verify_single_key_range(ranges)
+ ranges
+ .each_with_index
+ .all? do |range, i|
+ ranges
+ .slice((i + 1)..)
+ .all? { |elem| elem.overlaps? range }
+ end
+ end
+
+ # Converts paylaod component to dates range
+ #
+ # @param elem [Hash] payload component
+ #
+ # @return [Range]
+ def self.convert_es_date_range(elem)
+ range = elem[:range].first.last
+ from = parse_es_range_date range[:from] || range[:gt] || '-9999-01-01'
+ to = parse_es_range_date range[:to] || range[:lt] || '9999-01-01'
+
+ from..to
+ end
+
+ # Parses absolute date or converts relative date
+ #
+ # @param input [String] string representation of date
+ #
+ # @return [Range]
+ def self.parse_es_range_date(input)
+ match = input.match(%r{^now(-|\+)(\d+)(\w{1})$})
+
+ return DateTime.parse input if !match
+
+ map = {
+ d: 'day',
+ y: 'year',
+ M: 'month',
+ h: 'hour',
+ m: 'minute',
+ }
+
+ range = match.captures[1].to_i.send map[match.captures[2].to_sym]
+
+ case match.captures[0]
+ when '-'
+ range.ago
+ when '+'
+ range.from_now
+ end
+ end
end
diff --git a/lib/sequencer/sequence/import/freshdesk/permission_check.rb b/lib/sequencer/sequence/import/freshdesk/permission_check.rb
new file mode 100644
index 000000000..a9ea90520
--- /dev/null
+++ b/lib/sequencer/sequence/import/freshdesk/permission_check.rb
@@ -0,0 +1,22 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+class Sequencer
+ class Sequence
+ module Import
+ module Freshdesk
+ class PermissionCheck < Sequencer::Sequence::Base
+
+ def self.expecting
+ [:permission_present]
+ end
+
+ def self.sequence
+ [
+ 'Freshdesk::PermissionPresent',
+ ]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sequencer/unit/freshdesk/permission_present.rb b/lib/sequencer/unit/freshdesk/permission_present.rb
new file mode 100644
index 000000000..bbc96b315
--- /dev/null
+++ b/lib/sequencer/unit/freshdesk/permission_present.rb
@@ -0,0 +1,24 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+class Sequencer
+ class Unit
+ module Freshdesk
+ class PermissionPresent < Sequencer::Unit::Common::Provider::Named
+ extend ::Sequencer::Unit::Import::Freshdesk::Requester
+
+ private
+
+ def permission_present
+ response = self.class.perform_request(
+ api_path: 'agents',
+ )
+
+ response.is_a?(Net::HTTPOK)
+ rescue => e
+ logger.error e
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/zammad/db/reset.rake b/lib/tasks/zammad/db/reset.rake
index b88747ddf..b8a6b304e 100644
--- a/lib/tasks/zammad/db/reset.rake
+++ b/lib/tasks/zammad/db/reset.rake
@@ -11,16 +11,21 @@ namespace :zammad do
# execute them and their prerequisites multiple times (in tests)
# there is no way in rake to achieve that
%w[db:drop:_unsafe db:create db:migrate db:seed zammad:db:rebuild].each do |task|
+ case task
+ when 'db:migrate'
- if task == 'db:drop:_unsafe'
+ # make sure that old column schemas are getting dropped to prevent
+ # wrong schema for new db setup
+ ActiveRecord::Base.descendants.each(&:reset_column_information)
+
+ $stdout = StringIO.new
+ when 'db:drop:_unsafe'
# ensure all DB connections are closed before dropping the DB
# since Rails > 5.2 two connections are present (after `db:migrate`) that
# block dropping the DB for PostgreSQL
ActiveRecord::Base.connection_handler.connection_pools.each(&:disconnect!)
end
- $stdout = StringIO.new if task == 'db:migrate'.freeze
-
Rake::Task[task].reenable
Rake::Task[task].invoke
ensure
diff --git a/public/assets/chat/chat-no-jquery.coffee b/public/assets/chat/chat-no-jquery.coffee
index 7b3783778..21033ae09 100644
--- a/public/assets/chat/chat-no-jquery.coffee
+++ b/public/assets/chat/chat-no-jquery.coffee
@@ -1097,16 +1097,14 @@ do(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-no-jquery.js b/public/assets/chat/chat-no-jquery.js
index 843cf6eef..bb7c38869 100644
--- a/public/assets/chat/chat-no-jquery.js
+++ b/public/assets/chat/chat-no-jquery.js
@@ -1437,15 +1437,14 @@ var extend = function(child, parent) { for (var key in parent) { if (hasProp.cal
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-no-jquery.min.js b/public/assets/chat/chat-no-jquery.min.js
index 5e4a55a20..8c40ef151 100644
--- a/public/assets/chat/chat-no-jquery.min.js
+++ b/public/assets/chat/chat-no-jquery.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?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"+e;else{var s=qe(e,/^[\r\n\t ]+/);n=s&&s[0]}var o=T?T.createHTML(e):e;if(ge===pe)try{t=(new f).parseFromString(o,"text/html")}catch(e){}if(!t||!t.documentElement){t=z.createDocument(ge,"template",null);try{t.documentElement.innerHTML=fe?"":o}catch(e){}}var i=t.body||t.documentElement;return e&&n&&i.insertBefore(a.createTextNode(n),i.childNodes[0]||null),ge===pe?x.call(t,G?"html":"body")[0]:G?t.documentElement:i},Le=function(e){return A.call(e.ownerDocument||e,e,n.SHOW_ELEMENT|n.SHOW_COMMENT|n.SHOW_TEXT,null,!1)},xe=function(e){return"object"===(void 0===m?"undefined":ht(m))?e instanceof m:e&&"object"===(void 0===e?"undefined":ht(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},Oe=function(e,t,n){I[e]&&Re(I[e],function(e){e.call(u,t,n,ye)})},Ee=function(e){var t,n=void 0;if(Oe("beforeSanitizeElements",e,null),!((t=e)instanceof p||t instanceof g||"string"==typeof t.nodeName&&"string"==typeof t.textContent&&"function"==typeof t.removeChild&&t.attributes instanceof l&&"function"==typeof t.removeAttribute&&"function"==typeof t.setAttribute&&"string"==typeof t.namespaceURI&&"function"==typeof t.insertBefore))return ke(e),!0;if(qe(e.nodeName,/[\u0080-\uFFFF]/))return ke(e),!0;var s=Ne(e.nodeName);if(Oe("uponSanitizeElement",e,{tagName:s,allowedTags:q}),!xe(e.firstElementChild)&&(!xe(e.content)||!xe(e.content.firstElementChild))&&Be(/<[/\w]/g,e.innerHTML)&&Be(/<[/\w]/g,e.textContent))return ke(e),!0;if("select"===s&&Be(/ /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;l
new 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){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.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 without sessionId")}else 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('")}).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"+e;else{var s=qe(e,/^[\r\n\t ]+/);n=s&&s[0]}var o=T?T.createHTML(e):e;if(ge===pe)try{t=(new f).parseFromString(o,"text/html")}catch(e){}if(!t||!t.documentElement){t=z.createDocument(ge,"template",null);try{t.documentElement.innerHTML=fe?"":o}catch(e){}}var i=t.body||t.documentElement;return e&&n&&i.insertBefore(a.createTextNode(n),i.childNodes[0]||null),ge===pe?x.call(t,G?"html":"body")[0]:G?t.documentElement:i},Le=function(e){return A.call(e.ownerDocument||e,e,n.SHOW_ELEMENT|n.SHOW_COMMENT|n.SHOW_TEXT,null,!1)},xe=function(e){return"object"===(void 0===m?"undefined":ht(m))?e instanceof m:e&&"object"===(void 0===e?"undefined":ht(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},Oe=function(e,t,n){I[e]&&Re(I[e],function(e){e.call(u,t,n,ye)})},Ee=function(e){var t,n=void 0;if(Oe("beforeSanitizeElements",e,null),!((t=e)instanceof p||t instanceof g||"string"==typeof t.nodeName&&"string"==typeof t.textContent&&"function"==typeof t.removeChild&&t.attributes instanceof l&&"function"==typeof t.removeAttribute&&"function"==typeof t.setAttribute&&"string"==typeof t.namespaceURI&&"function"==typeof t.insertBefore))return ke(e),!0;if(qe(e.nodeName,/[\u0080-\uFFFF]/))return ke(e),!0;var s=Ne(e.nodeName);if(Oe("uponSanitizeElement",e,{tagName:s,allowedTags:q}),!xe(e.firstElementChild)&&(!xe(e.content)||!xe(e.content.firstElementChild))&&Be(/<[/\w]/g,e.innerHTML)&&Be(/<[/\w]/g,e.textContent))return ke(e),!0;if("select"===s&&Be(/ /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;l
new 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('")}).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"+e;else{var o=Fe(e,/^[\r\n\t ]+/);n=o&&o[0]}var s=C?C.createHTML(e):e;if(ge===pe)try{t=(new f).parseFromString(s,"text/html")}catch(e){}if(!t||!t.documentElement){t=z.createDocument(ge,"template",null);try{t.documentElement.innerHTML=fe?"":s}catch(e){}}var i=t.body||t.documentElement;return e&&n&&i.insertBefore(a.createTextNode(n),i.childNodes[0]||null),ge===pe?O.call(t,G?"html":"body")[0]:G?t.documentElement:i},xe=function(e){return A.call(e.ownerDocument||e,e,n.SHOW_ELEMENT|n.SHOW_COMMENT|n.SHOW_TEXT,null,!1)},Oe=function(e){return"object"===(void 0===m?"undefined":ht(m))?e instanceof m:e&&"object"===(void 0===e?"undefined":ht(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},Ie=function(e,t,n){_[e]&&Me(_[e],function(e){e.call(u,t,n,ye)})},Ee=function(e){var t,n=void 0;if(Ie("beforeSanitizeElements",e,null),!((t=e)instanceof p||t instanceof g||"string"==typeof t.nodeName&&"string"==typeof t.textContent&&"function"==typeof t.removeChild&&t.attributes instanceof l&&"function"==typeof t.removeAttribute&&"function"==typeof t.setAttribute&&"string"==typeof t.namespaceURI&&"function"==typeof t.insertBefore))return ke(e),!0;if(Fe(e.nodeName,/[\u0080-\uFFFF]/))return ke(e),!0;var o=Pe(e.nodeName);if(Ie("uponSanitizeElement",e,{tagName:o,allowedTags:F}),!Oe(e.firstElementChild)&&(!Oe(e.content)||!Oe(e.content.firstElementChild))&&Ue(/<[/\w]/g,e.innerHTML)&&Ue(/<[/\w]/g,e.textContent))return ke(e),!0;if("select"===o&&Ue(/ /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;t
new 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('")}).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"+e;else{var o=Fe(e,/^[\r\n\t ]+/);n=o&&o[0]}var s=C?C.createHTML(e):e;if(ge===pe)try{t=(new f).parseFromString(s,"text/html")}catch(e){}if(!t||!t.documentElement){t=z.createDocument(ge,"template",null);try{t.documentElement.innerHTML=fe?"":s}catch(e){}}var i=t.body||t.documentElement;return e&&n&&i.insertBefore(a.createTextNode(n),i.childNodes[0]||null),ge===pe?O.call(t,G?"html":"body")[0]:G?t.documentElement:i},xe=function(e){return A.call(e.ownerDocument||e,e,n.SHOW_ELEMENT|n.SHOW_COMMENT|n.SHOW_TEXT,null,!1)},Oe=function(e){return"object"===(void 0===m?"undefined":ht(m))?e instanceof m:e&&"object"===(void 0===e?"undefined":ht(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},Ie=function(e,t,n){_[e]&&Me(_[e],function(e){e.call(u,t,n,ye)})},Ee=function(e){var t,n=void 0;if(Ie("beforeSanitizeElements",e,null),!((t=e)instanceof p||t instanceof g||"string"==typeof t.nodeName&&"string"==typeof t.textContent&&"function"==typeof t.removeChild&&t.attributes instanceof l&&"function"==typeof t.removeAttribute&&"function"==typeof t.setAttribute&&"string"==typeof t.namespaceURI&&"function"==typeof t.insertBefore))return ke(e),!0;if(Fe(e.nodeName,/[\u0080-\uFFFF]/))return ke(e),!0;var o=Pe(e.nodeName);if(Ie("uponSanitizeElement",e,{tagName:o,allowedTags:F}),!Oe(e.firstElementChild)&&(!Oe(e.content)||!Oe(e.content.firstElementChild))&&Ue(/<[/\w]/g,e.innerHTML)&&Ue(/<[/\w]/g,e.textContent))return ke(e),!0;if("select"===o&&Ue(/ /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;t
Chat 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;t
new 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('")}).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',
}