Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20df373351 | ||
|
|
d3e3475aec | ||
|
|
c469f406cd | ||
|
|
bca4224393 | ||
|
|
eb9780155d | ||
|
|
7dadccdea0 | ||
|
|
7dc7fceeac | ||
|
|
df978093e7 | ||
|
|
8e7e0e467a | ||
|
|
9ec1d91366 | ||
|
|
ea52d8319c | ||
|
|
6b66f28818 | ||
|
|
fb063580ef | ||
|
|
c5b6308170 | ||
|
|
4206e70c3a | ||
|
|
0b14a3e114 | ||
|
|
317ec97e3d | ||
|
|
ab473b2f0d | ||
|
|
2a840a5e5a | ||
|
|
ed4ff7391d | ||
|
|
0066362558 | ||
|
|
f67686003a | ||
|
|
6e5310ea4f | ||
|
|
3477d67650 | ||
|
|
f71f7a103b | ||
|
|
2e162101be | ||
|
|
80ed32a94d | ||
|
|
c94b72cd1f | ||
|
|
5b4d53653f | ||
|
|
3463862759 | ||
|
|
92343c9cea | ||
|
|
36b8fb3499 | ||
|
|
fc034bd3d3 | ||
|
|
bd64be7971 | ||
|
|
280a1f7d04 | ||
|
|
388354080a | ||
|
|
4f93962ea4 | ||
|
|
a64597ff8d | ||
|
|
f62bd08c16 | ||
|
|
bf0bd12997 | ||
|
|
b0d6ca7a92 | ||
|
|
0a2a35683c | ||
|
|
1ac660da33 | ||
|
|
a084fc41c8 | ||
|
|
f176371556 | ||
|
|
94358429d6 | ||
|
|
1be88af5c1 | ||
|
|
08c6e3aa01 | ||
|
|
c2fa10175a | ||
|
|
83a46adb2d | ||
|
|
44ef54ecdd | ||
|
|
e6bf4d571c | ||
|
|
f5818ada93 | ||
|
|
89d4a33d5d | ||
|
|
d12552191f | ||
|
|
689a7cb148 | ||
|
|
dec7b134b5 | ||
|
|
f9db2a4728 | ||
|
|
5851f63fbb | ||
|
|
24ec9c959c | ||
|
|
1a09e50077 | ||
|
|
d6c25a2671 | ||
|
|
c62a36004c | ||
|
|
0a9f022d2e | ||
|
|
93b4cca236 | ||
|
|
00204dded0 | ||
|
|
5f503051fe | ||
|
|
c28298e71b | ||
|
|
979ea9caf0 | ||
|
|
c74fc76783 | ||
|
|
61874e0a23 | ||
|
|
bea0a2de1d | ||
|
|
ca75506667 | ||
|
|
30d94ce61d | ||
|
|
3196d8afee | ||
|
|
ff630ed1cb | ||
|
|
b8c8a0b5f7 | ||
|
|
3ee984d0f3 | ||
|
|
e91a7cae53 | ||
|
|
22cd7ccf3e | ||
|
|
e7b06654fd | ||
|
|
ccff1354ba | ||
|
|
0c1d519c5a | ||
|
|
0a0f22ec94 | ||
|
|
2628fee273 | ||
|
|
68c3bcad6c | ||
|
|
6e4a52b9d2 | ||
|
|
6a1a5fc02e | ||
|
|
215d1a42bc | ||
|
|
caec9d4328 | ||
|
|
045c5145ad | ||
|
|
743b98213e | ||
|
|
960910fef7 | ||
|
|
fb1ac41210 | ||
|
|
b62c81e55a | ||
|
|
e6eb44170b | ||
|
|
efab6b06f4 | ||
|
|
205433de6c | ||
|
|
ac194df03c | ||
|
|
80bf57c1b2 | ||
|
|
ea2eb62872 | ||
|
|
bf8b71d851 | ||
|
|
446f51f5e3 | ||
|
|
2f1bf5c5cf | ||
|
|
f76a83364f | ||
|
|
56c5f3a0c7 | ||
|
|
29bf395d54 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,5 +1,4 @@
|
||||
app/assets/javascripts/app/lib/animations/** linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/ba-linkify.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/clipboard.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/cropper.js linguist-vendored
|
||||
@@ -15,6 +14,7 @@ app/assets/javascripts/app/lib/base/jquery.fineuploader-3.0.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/jquery.hotkeys.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/jquery.noty.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/jquery.visible.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/linkify.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/marked.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/modernizr.custom.92138.js linguist-vendored
|
||||
app/assets/javascripts/app/lib/base/seedrandom.js linguist-vendored
|
||||
|
||||
@@ -26,3 +26,7 @@
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_db_unseeded[collapsed=true]\\r\\e[0Kbundle exec rake zammad:db:unseeded"
|
||||
- bundle exec rake zammad:db:unseeded
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_db_unseeded\\r\\e[0K"
|
||||
zammad_ci_test_prepare:
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_prepare[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:prepare"
|
||||
- bundle exec rake zammad:ci:test:prepare
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_prepare\\r\\e[0K"
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
.variables_es: &variables_es
|
||||
variables:
|
||||
ES_INDEX_RAND: 'true'
|
||||
ES_URL: 'http://elasticsearch:9200'
|
||||
ELASTICSEARCH_TAG: '7.16'
|
||||
|
||||
.variables_redis_memcached: &variables_redis_memcached
|
||||
variables:
|
||||
REDIS_URL: 'redis://redis:6379'
|
||||
MEMCACHE_SERVERS: 'memcached'
|
||||
|
||||
.variables_selenium_firefox: &variables_selenium_firefox
|
||||
.variables_selenium_firefox:
|
||||
variables:
|
||||
REMOTE_URL: 'http://selenium-firefox:4444/wd/hub'
|
||||
BROWSER: 'firefox'
|
||||
@@ -19,7 +8,7 @@
|
||||
SE_NODE_OVERRIDE_MAX_SESSIONS: 'true'
|
||||
SE_OPTS: '--log /builds/selenium.log'
|
||||
|
||||
.variables_selenium_chrome: &variables_selenium_chrome
|
||||
.variables_selenium_chrome:
|
||||
variables:
|
||||
REMOTE_URL: 'http://selenium-chrome:4444/wd/hub'
|
||||
BROWSER: chrome
|
||||
@@ -30,157 +19,40 @@
|
||||
SE_OPTS: '--log /builds/selenium.log'
|
||||
CHROME_LOG_FILE: '/builds/chrome.log'
|
||||
|
||||
# DB Docker
|
||||
.docker_mysql: &docker_mysql
|
||||
name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
.services:
|
||||
mysql:
|
||||
name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
|
||||
.docker_postgresql: &docker_postgresql
|
||||
name: $CI_REGISTRY/docker/zammad-postgresql:stable
|
||||
alias: postgresql
|
||||
mariadb:
|
||||
name: $CI_REGISTRY/docker/zammad-mariadb:stable
|
||||
alias: mysql
|
||||
|
||||
.docker_elasticsearch: &docker_elasticsearch
|
||||
name: $CI_REGISTRY/docker/zammad-elasticsearch:$ELASTICSEARCH_TAG
|
||||
alias: elasticsearch
|
||||
postgresql:
|
||||
name: $CI_REGISTRY/docker/zammad-postgresql:stable
|
||||
alias: postgresql
|
||||
|
||||
.docker_selenium_chrome: &docker_selenium_chrome
|
||||
name: $CI_REGISTRY/docker/zammad-selenium-chrome:stable
|
||||
alias: selenium-chrome
|
||||
elasticsearch:
|
||||
name: $CI_REGISTRY/docker/zammad-elasticsearch:$ELASTICSEARCH_TAG
|
||||
alias: elasticsearch
|
||||
|
||||
.docker_selenium_firefox: &docker_selenium_firefox
|
||||
name: $CI_REGISTRY/docker/zammad-selenium-firefox:stable
|
||||
alias: selenium-firefox
|
||||
selenium-chrome:
|
||||
name: $CI_REGISTRY/docker/zammad-selenium-chrome:stable
|
||||
alias: selenium-chrome
|
||||
|
||||
.docker_imap: &docker_imap
|
||||
name: $CI_REGISTRY/docker/zammad-imap:stable
|
||||
alias: mail
|
||||
selenium-firefox:
|
||||
name: $CI_REGISTRY/docker/zammad-selenium-firefox:stable
|
||||
alias: selenium-firefox
|
||||
|
||||
.docker_redis: &docker_redis
|
||||
name: $CI_REGISTRY/docker/zammad-redis:stable
|
||||
alias: redis
|
||||
imap:
|
||||
name: $CI_REGISTRY/docker/zammad-imap:stable
|
||||
alias: mail
|
||||
|
||||
.docker_memcached: &docker_memcached
|
||||
name: $CI_REGISTRY/docker/zammad-memcached:stable
|
||||
alias: memcached
|
||||
command: ['memcached', '-m', '256M']
|
||||
redis:
|
||||
name: $CI_REGISTRY/docker/zammad-redis:stable
|
||||
alias: redis
|
||||
|
||||
# service templates
|
||||
.services_mysql:
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
|
||||
.services_mysql_redis_memcached:
|
||||
extends:
|
||||
- .variables_redis_memcached
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_redis
|
||||
- <<: *docker_memcached
|
||||
|
||||
.services_postgresql:
|
||||
services:
|
||||
- <<: *docker_postgresql
|
||||
|
||||
.services_postgresql_redis_memcached:
|
||||
extends:
|
||||
- .variables_redis_memcached
|
||||
services:
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_redis
|
||||
- <<: *docker_memcached
|
||||
|
||||
.services_mysql_postgresql_redis_memcached:
|
||||
|
||||
extends:
|
||||
- .variables_redis_memcached
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_redis
|
||||
- <<: *docker_memcached
|
||||
|
||||
.services_postgresql_selenium_chrome:
|
||||
extends:
|
||||
- .variables_selenium_chrome
|
||||
services:
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_selenium_chrome
|
||||
|
||||
.services_postgresql_selenium_firefox:
|
||||
extends:
|
||||
- .variables_selenium_firefox
|
||||
services:
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_selenium_firefox
|
||||
|
||||
.services_mysql_postgresql_imap_redis_memcached:
|
||||
extends:
|
||||
- .variables_redis_memcached
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_imap
|
||||
- <<: *docker_redis
|
||||
- <<: *docker_memcached
|
||||
|
||||
.services_mysql_postgresql_elasticsearch:
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_elasticsearch
|
||||
|
||||
.services_mysql_postgresql_redis_memcached_elasticsearch:
|
||||
extends:
|
||||
- .variables_redis_memcached
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_redis
|
||||
- <<: *docker_memcached
|
||||
- <<: *docker_elasticsearch
|
||||
|
||||
.services_mysql_postgresql_elasticsearch_selenium_chrome_imap:
|
||||
extends:
|
||||
- .variables_selenium_chrome
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_elasticsearch
|
||||
- <<: *docker_selenium_chrome
|
||||
- <<: *docker_imap
|
||||
|
||||
.services_mysql_postgresql_elasticsearch_selenium_firefox_imap:
|
||||
extends:
|
||||
- .variables_selenium_firefox
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_elasticsearch
|
||||
- <<: *docker_selenium_firefox
|
||||
- <<: *docker_imap
|
||||
|
||||
.services_mysql_postgresql_elasticsearch_selenium_chrome_imap_redis_memcached:
|
||||
extends:
|
||||
- .variables_selenium_chrome
|
||||
- .variables_redis_memcached
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_elasticsearch
|
||||
- <<: *docker_selenium_chrome
|
||||
- <<: *docker_imap
|
||||
- <<: *docker_redis
|
||||
- <<: *docker_memcached
|
||||
|
||||
.services_mysql_postgresql_elasticsearch_selenium_firefox_imap_redis_memcached:
|
||||
extends:
|
||||
- .variables_selenium_firefox
|
||||
- .variables_redis_memcached
|
||||
services:
|
||||
- <<: *docker_mysql
|
||||
- <<: *docker_postgresql
|
||||
- <<: *docker_elasticsearch
|
||||
- <<: *docker_selenium_firefox
|
||||
- <<: *docker_imap
|
||||
- <<: *docker_redis
|
||||
- <<: *docker_memcached
|
||||
memcached:
|
||||
name: $CI_REGISTRY/docker/zammad-memcached:stable
|
||||
alias: memcached
|
||||
command: ['memcached', '-m', '256M']
|
||||
|
||||
@@ -17,6 +17,11 @@ variables:
|
||||
MAIL_SERVER_EMAIL: "zammad@mail.test.dc.zammad.com"
|
||||
KEEP_ON_MAIL_SERVER: "mail"
|
||||
KEEP_ON_MAIL_SERVER_ACCOUNT: "zammad@mail.test.dc.zammad.com:zammad"
|
||||
|
||||
# Settings for elasticsearch, if active
|
||||
ES_INDEX_RAND: 'true'
|
||||
ELASTICSEARCH_TAG: '7.16'
|
||||
|
||||
# Temporary switch to enable the mobile front end for testing.
|
||||
ENABLE_EXPERIMENTAL_MOBILE_FRONTEND: 'true'
|
||||
# Compile assets only once.
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.template_browser-core: &template_browser-core
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .variables_es
|
||||
- .tags_docker_low_concurrency
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
script:
|
||||
- script/build/test_slice_tests.sh $TEST_SLICE
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start[with_elasticsearch]
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- time bundle exec rails test --fail-fast test/browser
|
||||
|
||||
.template_browser-core_chrome: &template_browser-core_chrome
|
||||
extends:
|
||||
- .template_browser-core
|
||||
- .services_mysql_postgresql_elasticsearch_selenium_chrome_imap_redis_memcached
|
||||
|
||||
.template_browser-core_firefox: &template_browser-core_firefox
|
||||
extends:
|
||||
- .template_browser-core
|
||||
- .services_mysql_postgresql_elasticsearch_selenium_firefox_imap_redis_memcached
|
||||
|
||||
.template_browser-core_autowizard: &template_browser-core_autowizard
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .tags_docker_low_concurrency
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
AUTO_WIZARD_FILE: "contrib/auto_wizard_example.json"
|
||||
TEST_FILE: "test/integration/auto_wizard_browser_test.rb"
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- cp $AUTO_WIZARD_FILE auto_wizard.json
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- bundle exec rails test $TEST_FILE
|
||||
|
||||
.template_browser-core_autowizard_chrome: &template_browser-core_autowizard_chrome
|
||||
extends:
|
||||
- .template_browser-core_autowizard
|
||||
- .services_postgresql_selenium_chrome
|
||||
|
||||
.template_browser-core_autowizard_firefox: &template_browser-core_autowizard_firefox
|
||||
extends:
|
||||
- .template_browser-core_autowizard
|
||||
- .services_postgresql_selenium_firefox
|
||||
|
||||
.template_browser-core_capybara: &template_browser-core_capybara
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .variables_es
|
||||
- .tags_docker_low_concurrency
|
||||
script:
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_prepare[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:prepare"
|
||||
- bundle exec rake zammad:ci:test:prepare
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_prepare\\r\\e[0K"
|
||||
- echo "RSpec filter:" "$RSPEC_EXAMPLE_MATCHES"
|
||||
- bundle exec rspec --require ./spec/rspec_extensions.rb --fail-fast -t ~integration --profile 10 spec/system
|
||||
|
||||
.template_browser-core_capybara_chrome: &template_browser-core_capybara_chrome
|
||||
extends:
|
||||
- .template_browser-core_capybara
|
||||
- .services_mysql_postgresql_elasticsearch_selenium_chrome_imap_redis_memcached
|
||||
|
||||
.template_browser-core_capybara_firefox: &template_browser-core_capybara_firefox
|
||||
extends:
|
||||
- .template_browser-core_capybara
|
||||
- .services_mysql_postgresql_elasticsearch_selenium_firefox_imap_redis_memcached
|
||||
@@ -1,62 +0,0 @@
|
||||
.template_browser-integration_capybara: &template_browser-integration_capybara
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .variables_es
|
||||
- .tags_docker_low_concurrency
|
||||
script:
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_prepare[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:prepare"
|
||||
- bundle exec rake zammad:ci:test:prepare
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_prepare\\r\\e[0K"
|
||||
- bundle exec rspec --fail-fast --pattern "spec/system/**/*_spec.rb" -t integration --profile 10
|
||||
|
||||
.template_browser-integration_capybara_chrome: &template_browser-integration_capybara_chrome
|
||||
extends:
|
||||
- .template_browser-integration_capybara
|
||||
- .services_mysql_postgresql_elasticsearch_selenium_chrome_imap_redis_memcached
|
||||
|
||||
.template_browser-integration_capybara_firefox: &template_browser-integration_capybara_firefox
|
||||
extends:
|
||||
- .template_browser-integration_capybara
|
||||
- .services_mysql_postgresql_elasticsearch_selenium_firefox_imap_redis_memcached
|
||||
|
||||
.template_browser-integration_chrome: &template_browser-integration_chrome
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .services_postgresql_selenium_chrome
|
||||
- .tags_docker_low_concurrency
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
AUTO_WIZARD_FILE: "contrib/auto_wizard_test.json"
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- cp $AUTO_WIZARD_FILE auto_wizard.json
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- bundle exec rails test $TEST_FILE
|
||||
|
||||
.template_browser-integration_import_chrome: &template_browser-integration_import_chrome
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .services_postgresql_selenium_chrome
|
||||
- .tags_docker_low_concurrency
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
RAILS_SERVE_STATIC_FILES: "true"
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- bundle exec rails test $TEST_FILE
|
||||
@@ -1,7 +0,0 @@
|
||||
'integration:capybara:chrome':
|
||||
extends:
|
||||
- .template_browser-integration_capybara_chrome
|
||||
|
||||
'integration:capybara:firefox':
|
||||
extends:
|
||||
- .template_browser-integration_capybara_firefox
|
||||
@@ -1,3 +1,2 @@
|
||||
include:
|
||||
- '.gitlab/ci/browser-core/*.yml'
|
||||
- '.gitlab/ci/browser-integration/*.yml'
|
||||
- '.gitlab/ci/browser/*.yml'
|
||||
|
||||
149
.gitlab/ci/browser/__templates__.yml
Normal file
149
.gitlab/ci/browser/__templates__.yml
Normal file
@@ -0,0 +1,149 @@
|
||||
.template_browser-core:
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .tags_docker_low_concurrency
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
script:
|
||||
- script/build/test_slice_tests.sh $TEST_SLICE
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start[with_elasticsearch]
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- time bundle exec rails test --fail-fast test/browser
|
||||
|
||||
.template_browser-core_chrome:
|
||||
extends:
|
||||
- .template_browser-core
|
||||
- .variables_selenium_chrome
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, elasticsearch]
|
||||
- !reference [.services, selenium-chrome]
|
||||
- !reference [.services, imap]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
.template_browser-core_firefox:
|
||||
extends:
|
||||
- .template_browser-core
|
||||
- .variables_selenium_firefox
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, elasticsearch]
|
||||
- !reference [.services, selenium-firefox]
|
||||
- !reference [.services, imap]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
.template_browser-core_autowizard:
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .tags_docker_low_concurrency
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
AUTO_WIZARD_FILE: "contrib/auto_wizard_example.json"
|
||||
TEST_FILE: "test/integration/auto_wizard_browser_test.rb"
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- cp $AUTO_WIZARD_FILE auto_wizard.json
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- bundle exec rails test $TEST_FILE
|
||||
|
||||
.template_browser-core_autowizard_chrome:
|
||||
extends:
|
||||
- .template_browser-core_autowizard
|
||||
- .variables_selenium_chrome
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, selenium-chrome]
|
||||
|
||||
.template_browser-core_autowizard_firefox:
|
||||
extends:
|
||||
- .template_browser-core_autowizard
|
||||
- .variables_selenium_firefox
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, selenium-firefox]
|
||||
|
||||
.template_browser-core_capybara:
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .tags_docker_low_concurrency
|
||||
script:
|
||||
- !reference [.scripts, zammad_ci_test_prepare]
|
||||
- bundle exec rspec --require ./spec/rspec_extensions.rb --fail-fast --profile 10 spec/system
|
||||
|
||||
.template_browser-core_capybara_chrome:
|
||||
extends:
|
||||
- .template_browser-core_capybara
|
||||
- .variables_selenium_chrome
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, elasticsearch]
|
||||
- !reference [.services, selenium-chrome]
|
||||
- !reference [.services, imap]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
.template_browser-core_capybara_firefox:
|
||||
extends:
|
||||
- .template_browser-core_capybara
|
||||
- .variables_selenium_firefox
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, elasticsearch]
|
||||
- !reference [.services, selenium-firefox]
|
||||
- !reference [.services, imap]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
.template_browser-integration_chrome:
|
||||
stage: browser
|
||||
dependencies:
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .tags_docker_low_concurrency
|
||||
- .variables_selenium_chrome
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, selenium-chrome]
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
AUTO_WIZARD_FILE: "contrib/auto_wizard_test.json"
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- cp $AUTO_WIZARD_FILE auto_wizard.json
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- bundle exec rails test $TEST_FILE
|
||||
|
||||
.template_browser-integration_import_chrome:
|
||||
extends:
|
||||
- .template_browser-integration_chrome
|
||||
script:
|
||||
- RAILS_ENV=test bundle exec rake db:create
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_ci_test_start[collapsed=true]\\r\\e[0Kbundle exec rake zammad:ci:test:start"
|
||||
- bundle exec rake zammad:ci:test:start
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_ci_test_start\\r\\e[0K"
|
||||
- bundle exec rails test $TEST_FILE
|
||||
14
.gitlab/ci/browser/capybara_mariadb.yml
Normal file
14
.gitlab/ci/browser/capybara_mariadb.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
capybara:mariadb:
|
||||
extends:
|
||||
- .template_browser-core_capybara
|
||||
- .variables_selenium_chrome
|
||||
services:
|
||||
- !reference [.services, mariadb]
|
||||
- !reference [.services, elasticsearch]
|
||||
- !reference [.services, selenium-chrome]
|
||||
- !reference [.services, imap]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
script:
|
||||
- !reference [.scripts, zammad_ci_test_prepare]
|
||||
- bundle exec rspec --require ./spec/rspec_extensions.rb --fail-fast -t mariadb --profile 10 spec/system
|
||||
@@ -1,13 +1,14 @@
|
||||
# Workaround to enable usage of mixed SSH and Docker GitLab CI runners
|
||||
.template_lint: &template_lint
|
||||
.template_lint:
|
||||
stage: lint
|
||||
extends:
|
||||
- .job_rules_default
|
||||
|
||||
.template_lint_rails: &template_lint_rails
|
||||
.template_lint_rails:
|
||||
extends:
|
||||
- .template_lint
|
||||
- .services_postgresql
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
before_script:
|
||||
- !reference [.scripts, source_rvm]
|
||||
- !reference [.scripts, bundle_install]
|
||||
@@ -16,7 +17,8 @@
|
||||
|
||||
# Must be a separate job because it uses a custom image.
|
||||
'lint: shellcheck':
|
||||
<<: *template_lint
|
||||
extends:
|
||||
- .template_lint
|
||||
image: koalaman/shellcheck-alpine:stable
|
||||
cache: []
|
||||
before_script: []
|
||||
@@ -26,7 +28,8 @@
|
||||
after_script: []
|
||||
|
||||
'lint: i18n & rails':
|
||||
<<: *template_lint_rails
|
||||
extends:
|
||||
- .template_lint_rails
|
||||
cache: !reference [.cache, read_only_full]
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
@@ -46,7 +49,8 @@
|
||||
- .gitlab/check_graphql_api_consistency.sh
|
||||
|
||||
'lint: ruby & security':
|
||||
<<: *template_lint
|
||||
extends:
|
||||
- .template_lint
|
||||
before_script:
|
||||
- !reference [.scripts, source_rvm]
|
||||
- !reference [.scripts, bundle_install]
|
||||
@@ -60,7 +64,8 @@
|
||||
- bundle-audit --ignore CVE-2015-9284
|
||||
|
||||
'lint: js & css':
|
||||
<<: *template_lint
|
||||
extends:
|
||||
- .template_lint
|
||||
cache: !reference [.cache, read_only_nodejs]
|
||||
before_script: []
|
||||
script:
|
||||
@@ -77,7 +82,8 @@
|
||||
|
||||
# Must be a separate job because it may fail and is only executed manually.
|
||||
'lint: orphaned ruby gems':
|
||||
<<: *template_lint_rails
|
||||
extends:
|
||||
- .template_lint_rails
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_ID
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
- script/build/sync_repo.sh git@github.com:zammad/zammad.git
|
||||
|
||||
'refresh global envs':
|
||||
extends:
|
||||
- .services_postgresql
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
stage: pre
|
||||
needs:
|
||||
- 'write runner cache: ruby'
|
||||
|
||||
@@ -4,8 +4,12 @@
|
||||
- browser:build
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .services_mysql_postgresql_redis_memcached
|
||||
- .tags_docker_low_concurrency
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
variables:
|
||||
RAILS_ENV: "production"
|
||||
APP_RESTART_CMD: 'bundle exec rake environment zammad:ci:app:restart'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
browser:build:
|
||||
stage: test
|
||||
extends:
|
||||
- .services_postgresql
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
include:
|
||||
- '.gitlab/ci/test/integration/*.yml'
|
||||
|
||||
.template_integration: &template_integration
|
||||
.template_integration:
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .services_mysql_postgresql_imap_redis_memcached
|
||||
- .job_rules_integration_manual_start
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, imap]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
stage: test
|
||||
@@ -1,8 +1,12 @@
|
||||
.template_integration_es: &template_integration_es
|
||||
.template_integration_es:
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .services_mysql_postgresql_redis_memcached_elasticsearch
|
||||
- .variables_es
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
- !reference [.services, elasticsearch]
|
||||
stage: test
|
||||
script:
|
||||
- !reference [.scripts, zammad_db_unseeded]
|
||||
@@ -12,18 +16,21 @@
|
||||
- bundle exec rspec --tag searchindex --tag ~type:system --profile 10
|
||||
|
||||
# Lowest supported version
|
||||
'rspec:es:7.8':
|
||||
<<: *template_integration_es
|
||||
rspec:es:7.8:
|
||||
extends:
|
||||
- .template_integration_es
|
||||
variables:
|
||||
ELASTICSEARCH_TAG: '7.8.1'
|
||||
|
||||
# Highest supported version (except for newer releases that are not yet in CI)
|
||||
'rspec:es:7.16':
|
||||
<<: *template_integration_es
|
||||
rspec:es:7.16:
|
||||
extends:
|
||||
- .template_integration_es
|
||||
variables:
|
||||
ELASTICSEARCH_TAG: '7.16'
|
||||
|
||||
'rspec:es:8.1':
|
||||
<<: *template_integration_es
|
||||
rspec:es:8.1:
|
||||
extends:
|
||||
- .template_integration_es
|
||||
variables:
|
||||
ELASTICSEARCH_TAG: '8.1'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.template_integration_otrs: &template_integration_otrs
|
||||
.template_integration_otrs:
|
||||
extends:
|
||||
- .template_integration
|
||||
script:
|
||||
@@ -9,8 +9,9 @@
|
||||
IMPORT_OTRS_ENDPOINT: 'http://zammad-ci-otrsimport-app/otrs/public.pl?Action=ZammadMigrator'
|
||||
TZ: 'Europe/Berlin' # Required for the zammad-ci-otrsimport-app containers
|
||||
|
||||
'minitest:integration:otrs:6':
|
||||
<<: *template_integration_otrs
|
||||
minitest:integration:otrs:6:
|
||||
extends:
|
||||
- .template_integration_otrs
|
||||
services:
|
||||
- name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
@@ -26,8 +27,9 @@
|
||||
- name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs6
|
||||
alias: zammad-ci-otrsimport-app
|
||||
|
||||
'minitest:integration:otrs:5':
|
||||
<<: *template_integration_otrs
|
||||
minitest:integration:otrs:5:
|
||||
extends:
|
||||
- .template_integration_otrs
|
||||
services:
|
||||
- name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
@@ -43,8 +45,9 @@
|
||||
- name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs5
|
||||
alias: zammad-ci-otrsimport-app
|
||||
|
||||
'minitest:integration:otrs:4':
|
||||
<<: *template_integration_otrs
|
||||
minitest:integration:otrs:4:
|
||||
extends:
|
||||
- .template_integration_otrs
|
||||
services:
|
||||
- name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
@@ -60,8 +63,9 @@
|
||||
- name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs4
|
||||
alias: zammad-ci-otrsimport-app
|
||||
|
||||
'minitest:integration:otrs:33':
|
||||
<<: *template_integration_otrs
|
||||
minitest:integration:otrs:33:
|
||||
extends:
|
||||
- .template_integration_otrs
|
||||
services:
|
||||
- name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
@@ -77,8 +81,9 @@
|
||||
- name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs33
|
||||
alias: zammad-ci-otrsimport-app
|
||||
|
||||
'minitest:integration:otrs:32':
|
||||
<<: *template_integration_otrs
|
||||
minitest:integration:otrs:32:
|
||||
extends:
|
||||
- .template_integration_otrs
|
||||
services:
|
||||
- name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
@@ -94,8 +99,9 @@
|
||||
- name: $CI_REGISTRY/docker/zammad-ci-otrsimport-app:otrs32
|
||||
alias: zammad-ci-otrsimport-app
|
||||
|
||||
'minitest:integration:otrs:31':
|
||||
<<: *template_integration_otrs
|
||||
minitest:integration:otrs:31:
|
||||
extends:
|
||||
- .template_integration_otrs
|
||||
services:
|
||||
- name: $CI_REGISTRY/docker/zammad-mysql:stable
|
||||
alias: mysql
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
include:
|
||||
- '.gitlab/ci/test/rspec/*.yml'
|
||||
|
||||
.template_rspec: &template_rspec
|
||||
.template_rspec:
|
||||
stage: test
|
||||
extends:
|
||||
- .job_rules_default
|
||||
script:
|
||||
- echo -e "\\e[0Ksection_start:`date +%s`:zammad_db_init[collapsed=true]\\r\\e[0Kbundle exec rake zammad:db:init"
|
||||
- bundle exec rake zammad:db:init
|
||||
- echo -e "\\e[0Ksection_end:`date +%s`:zammad_db_init\\r\\e[0K"
|
||||
- echo "RSpec filter:" "$RSPEC_EXAMPLE_MATCHES"
|
||||
- bundle exec rspec --require ./spec/rspec_extensions.rb --exclude-pattern "spec/{system,integration}/**/*_spec.rb" -t ~searchindex --profile 10
|
||||
- !reference [.scripts, zammad_db_init]
|
||||
- bundle exec rspec --require ./spec/rspec_extensions.rb --exclude-pattern "spec/system/**/*_spec.rb" -t ~searchindex -t ~integration --profile 10
|
||||
|
||||
# RSpec 3rd party integration tests are independent of the database
|
||||
# therefore we need only one job and choose a random database for each run
|
||||
@@ -19,8 +17,13 @@ rspec:integration:
|
||||
stage: test
|
||||
extends:
|
||||
- .job_rules_default
|
||||
- .services_mysql_postgresql_imap_redis_memcached
|
||||
- .job_rules_integration_manual_start
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, imap]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
script:
|
||||
- !reference [.scripts, zammad_db_init]
|
||||
- bundle exec rspec -t type:integration --profile 10
|
||||
- bundle exec rspec --exclude-pattern "spec/system/**/*_spec.rb" -t integration --profile 10
|
||||
|
||||
11
.gitlab/ci/test/rspec/mariadb.yml
Normal file
11
.gitlab/ci/test/rspec/mariadb.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
rspec:mariadb:
|
||||
extends:
|
||||
- .template_rspec
|
||||
services:
|
||||
- !reference [.services, mariadb]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
script:
|
||||
- !reference [.scripts, zammad_db_init]
|
||||
# Execute only tests tagged with :mariadb.
|
||||
- bundle exec rspec --require ./spec/rspec_extensions.rb --exclude-pattern "spec/system/**/*_spec.rb" -t mariadb --profile 10
|
||||
@@ -1,53 +1,49 @@
|
||||
.template_rspec_mysql:
|
||||
extends:
|
||||
- .template_rspec
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
rspec:mysql:1:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 1
|
||||
extends:
|
||||
- .services_mysql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_mysql
|
||||
|
||||
rspec:mysql:2:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 2
|
||||
extends:
|
||||
- .services_mysql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_mysql
|
||||
|
||||
rspec:mysql:3:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 3
|
||||
extends:
|
||||
- .services_mysql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_mysql
|
||||
|
||||
rspec:mysql:4:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 4
|
||||
extends:
|
||||
- .services_mysql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_mysql
|
||||
|
||||
rspec:mysql:5:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 5
|
||||
extends:
|
||||
- .services_mysql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_mysql
|
||||
|
||||
rspec:mysql:6:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 6
|
||||
extends:
|
||||
- .services_mysql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_mysql
|
||||
|
||||
@@ -1,53 +1,49 @@
|
||||
.template_rspec_postgresql:
|
||||
extends:
|
||||
- .template_rspec
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
rspec:postgresql:1:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 1
|
||||
extends:
|
||||
- .services_postgresql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_postgresql
|
||||
|
||||
rspec:postgresql:2:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 2
|
||||
extends:
|
||||
- .services_postgresql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_postgresql
|
||||
|
||||
rspec:postgresql:3:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 3
|
||||
extends:
|
||||
- .services_postgresql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_postgresql
|
||||
|
||||
rspec:postgresql:4:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 4
|
||||
extends:
|
||||
- .services_postgresql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_postgresql
|
||||
|
||||
rspec:postgresql:5:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 5
|
||||
extends:
|
||||
- .services_postgresql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_postgresql
|
||||
|
||||
rspec:postgresql:6:
|
||||
stage: test
|
||||
variables:
|
||||
RSPEC_SLICES: 6
|
||||
RSPEC_CURRENT_SLICE: 6
|
||||
extends:
|
||||
- .services_postgresql_redis_memcached
|
||||
- .template_rspec
|
||||
- .template_rspec_postgresql
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.template_unit: &template_unit
|
||||
.template_unit:
|
||||
extends:
|
||||
- .job_rules_default
|
||||
script:
|
||||
@@ -10,11 +10,17 @@
|
||||
unit:mysql:
|
||||
stage: test
|
||||
extends:
|
||||
- .services_mysql_redis_memcached
|
||||
- .template_unit
|
||||
services:
|
||||
- !reference [.services, mysql]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
unit:postgresql:
|
||||
stage: test
|
||||
extends:
|
||||
- .services_postgresql_redis_memcached
|
||||
- .template_unit
|
||||
- .template_unit
|
||||
services:
|
||||
- !reference [.services, postgresql]
|
||||
- !reference [.services, redis]
|
||||
- !reference [.services, memcached]
|
||||
|
||||
@@ -7,8 +7,10 @@ require 'fileutils'
|
||||
|
||||
#
|
||||
# Configures the CI system
|
||||
# - either (randomly) mysql or postgresql, if it is available
|
||||
# - (randomly) mysql or postgresql, if available
|
||||
# - (randomly) Redis or File as web socket session back end, if Redis is available
|
||||
# - (randomly) Memcached or File as Rails cache store, if Memcached is available
|
||||
# - Elasticsearch support, if available
|
||||
#
|
||||
# Database config happens directly in config/database.yml, other settings are written to
|
||||
# .gitlab/environment.env which must be sourced in the CI configuration.
|
||||
@@ -22,7 +24,35 @@ class ConfigureEnvironment
|
||||
true
|
||||
ENV_FILE_CONTENT
|
||||
|
||||
def self.configure_database # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
||||
DB_SETTINGS_MAP = {
|
||||
'postgresql' => {
|
||||
'adapter' => 'postgresql',
|
||||
'username' => 'zammad',
|
||||
'password' => 'zammad',
|
||||
'host' => 'postgresql', # db alias from gitlab-ci.yml
|
||||
},
|
||||
'mysql' => {
|
||||
'adapter' => 'mysql2',
|
||||
'username' => 'root',
|
||||
'password' => 'zammad',
|
||||
'host' => 'mysql', # db alias from gitlab-ci.yml
|
||||
}
|
||||
}.freeze
|
||||
|
||||
# Detect service availability based on host presence in network.
|
||||
def self.network_host_exists?(hostname)
|
||||
if %w[1 true].include?(ENV['FF_NETWORK_PER_BUILD'])
|
||||
begin
|
||||
!!Resolv::DNS.new.tap { |dns| dns.timeouts = 3 }.getaddress(hostname)
|
||||
rescue Resolv::ResolvError
|
||||
false
|
||||
end
|
||||
else
|
||||
File.foreach('/etc/hosts').any? { |l| l[hostname] }
|
||||
end
|
||||
end
|
||||
|
||||
def self.configure_database # rubocop:disable Metrics/AbcSize
|
||||
|
||||
if File.exist? File.join(__dir__, '../config/database.yml')
|
||||
puts "'config/database.yml' already exists and will not be changed."
|
||||
@@ -32,51 +62,16 @@ class ConfigureEnvironment
|
||||
cnf = YAML.load_file(File.join(__dir__, '../config/database/database.yml'))
|
||||
cnf.delete('default')
|
||||
|
||||
database = ENV['ENFORCE_DB_SERVICE']
|
||||
|
||||
# Lookup in /etc/hosts first: gitlab uses that if FF_NETWORK_PER_BUILD is not set.
|
||||
if !database
|
||||
hostsfile = '/etc/hosts'
|
||||
database = %w[postgresql mysql].shuffle.find do |possible_database|
|
||||
File.foreach(hostsfile).any? { |l| l[possible_database] }
|
||||
end
|
||||
end
|
||||
|
||||
# Lookup via DNS if needed: gitlab uses that if FF_NETWORK_PER_BUILD is enabled.
|
||||
if !database
|
||||
dns = Resolv::DNS.new
|
||||
dns.timeouts = 3
|
||||
database = %w[postgresql mysql].shuffle.find do |possible_database|
|
||||
# Perform a lookup of the database host to check if it is configured as a service.
|
||||
if dns.getaddress possible_database
|
||||
next possible_database
|
||||
end
|
||||
rescue Resolv::ResolvError
|
||||
# Ignore DNS lookup errors
|
||||
end
|
||||
database = ENV['ENFORCE_DB_SERVICE'] || %w[postgresql mysql].shuffle.find do |db|
|
||||
network_host_exists?(db)
|
||||
end
|
||||
|
||||
raise "Can't find any supported database." if database.nil?
|
||||
|
||||
puts "Using #{database} as database service."
|
||||
|
||||
db_settings_map = {
|
||||
'postgresql' => {
|
||||
'adapter' => 'postgresql',
|
||||
'username' => 'zammad',
|
||||
'password' => 'zammad',
|
||||
'host' => 'postgresql', # db alias from gitlab-ci.yml
|
||||
},
|
||||
'mysql' => {
|
||||
'adapter' => 'mysql2',
|
||||
'username' => 'root',
|
||||
'password' => 'zammad',
|
||||
'host' => 'mysql', # db alias from gitlab-ci.yml
|
||||
}
|
||||
}
|
||||
|
||||
# fetch DB settings from settings map and fallback to postgresql
|
||||
db_settings = db_settings_map.fetch(database) { db_settings_map['postgresql'] }
|
||||
db_settings = DB_SETTINGS_MAP.fetch(database) { DB_SETTINGS_MAP['postgresql'] }
|
||||
|
||||
%w[development test production].each do |environment|
|
||||
cnf[environment].merge!(db_settings)
|
||||
@@ -85,40 +80,46 @@ class ConfigureEnvironment
|
||||
File.write(File.join(__dir__, '../config/database.yml'), Psych.dump(cnf))
|
||||
end
|
||||
|
||||
def self.configure_redis # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
||||
puts 'ENABLING THE NEW EXPERIMENTAL MOBILE FRONTEND.' if ENV['ENABLE_EXPERIMENTAL_MOBILE_FRONTEND'] == 'true'
|
||||
if ENV['ENABLE_EXPERIMENTAL_MOBILE_FRONTEND'] == 'true' && (ENV['REDIS_URL'].nil? || ENV['REDIS_URL'].empty?) # rubocop:disable Rails/Blank
|
||||
if database_type == 'mysql'
|
||||
raise 'Redis was not found, but is required for ActionCable on MySQL based systems.'
|
||||
end
|
||||
def self.configure_redis
|
||||
has_redis = network_host_exists?('redis')
|
||||
needs_redis = database_type == 'mysql' && ENV['ENABLE_EXPERIMENTAL_MOBILE_FRONTEND'] == 'true'
|
||||
|
||||
puts 'Redis is not available, using File as web socket session store.'
|
||||
puts 'Redis is not available, using the PostgreSQL adapter for ActionCable.'
|
||||
if needs_redis && !has_redis
|
||||
raise 'Redis was not found, but is required for ActionCable on MySQL based systems.'
|
||||
end
|
||||
|
||||
if has_redis && [true, needs_redis].sample
|
||||
puts 'Using Redis as web socket session store and as adapter for ActionCable.'
|
||||
@env_file_content += "export REDIS_URL='redis://redis:6379'\n"
|
||||
return
|
||||
end
|
||||
if database_type == 'mysql' || [true, false].sample
|
||||
puts 'Using Redis as web socket session store.'
|
||||
puts 'Using the Redis adapter for ActionCable.'
|
||||
return
|
||||
end
|
||||
puts 'Using File as web socket session store.'
|
||||
puts 'Using the PostgreSQL adapter for ActionCable.'
|
||||
|
||||
puts 'Using File as web socket session store and the PostgreSQL adapter for ActionCable.'
|
||||
@env_file_content += "unset REDIS_URL\n"
|
||||
end
|
||||
|
||||
def self.configure_memcached
|
||||
if ENV['MEMCACHE_SERVERS'].nil? || ENV['MEMCACHE_SERVERS'].empty? # rubocop:disable Rails/Blank
|
||||
puts 'Memcached is not available, using File as Rails cache store.'
|
||||
return
|
||||
end
|
||||
if [true, false].sample
|
||||
if network_host_exists?('memcached') && [true, false].sample
|
||||
puts 'Using memcached as Rails cache store.'
|
||||
@env_file_content += "export MEMCACHE_SERVERS='memcached'\n"
|
||||
return
|
||||
end
|
||||
|
||||
puts "Using Zammad's file store as Rails cache store."
|
||||
@env_file_content += "unset MEMCACHE_SERVERS\n"
|
||||
end
|
||||
|
||||
def self.configure_elasticsearch
|
||||
if network_host_exists?('elasticsearch')
|
||||
puts 'Activating support for Elasticsearch.'
|
||||
@env_file_content += "export ES_URL='http://elasticsearch:9200'\n"
|
||||
return
|
||||
end
|
||||
|
||||
puts 'Not using Elasticsearch.'
|
||||
@env_file_content += "unset ES_URL\n"
|
||||
end
|
||||
|
||||
# Since configure_database skips if database.yml already exists, check the
|
||||
# content of that file to reliably determine the database type in all cases.
|
||||
def self.database_type
|
||||
@@ -135,9 +136,11 @@ class ConfigureEnvironment
|
||||
end
|
||||
|
||||
def self.run
|
||||
puts 'ENABLING THE NEW EXPERIMENTAL MOBILE FRONTEND.' if ENV['ENABLE_EXPERIMENTAL_MOBILE_FRONTEND'] == 'true'
|
||||
configure_database
|
||||
configure_redis
|
||||
configure_memcached
|
||||
configure_elasticsearch
|
||||
write_env_file
|
||||
end
|
||||
end
|
||||
|
||||
@@ -339,6 +339,14 @@ RSpec/Rails/AvoidSetupHook:
|
||||
Exclude:
|
||||
- "test/**/*"
|
||||
|
||||
RSpec/AlignLeftLetBrace:
|
||||
Description: 'Checks that left braces for adjacent single line lets are aligned.'
|
||||
Enabled: true
|
||||
|
||||
RSpec/AnyInstance:
|
||||
Description: 'Prefer instance doubles over stubbing any instance of a class.'
|
||||
Enabled: false
|
||||
|
||||
Zammad/PreferNegatedIfOverUnless:
|
||||
Exclude:
|
||||
- 'bin/rspec'
|
||||
@@ -379,4 +387,4 @@ Zammad/ExistsDbStrategy:
|
||||
Include:
|
||||
- "spec/**/*.rb"
|
||||
Exclude:
|
||||
- spec/support/capybara/common_actions.rb
|
||||
- "spec/support/capybara/*.rb"
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
RSpec/AnyInstance:
|
||||
Exclude:
|
||||
- 'spec/lib/import/otrs/article_spec.rb'
|
||||
- 'spec/lib/import/otrs/customer_spec.rb'
|
||||
- 'spec/lib/import/otrs/customer_user_spec.rb'
|
||||
- 'spec/lib/import/otrs/priority_spec.rb'
|
||||
- 'spec/lib/import/otrs/queue_spec.rb'
|
||||
- 'spec/lib/import/otrs/state_spec.rb'
|
||||
- 'spec/lib/import/otrs/ticket_spec.rb'
|
||||
- 'spec/lib/import/otrs/user_spec.rb'
|
||||
- 'spec/models/import_job_spec.rb'
|
||||
|
||||
RSpec/BeforeAfterAll:
|
||||
Exclude:
|
||||
- 'spec/spec_helper.rb'
|
||||
@@ -191,7 +179,6 @@ RSpec/ContextWording:
|
||||
- 'spec/system/system/object_manager_spec.rb'
|
||||
- 'spec/system/system/translations_spec.rb'
|
||||
- 'spec/system/ticket/create_spec.rb'
|
||||
- 'spec/system/ticket/inserting_knowledge_base_answer_spec.rb'
|
||||
- 'spec/system/ticket/shared_draft_start_spec.rb'
|
||||
- 'spec/system/ticket/shared_draft_zoom_spec.rb'
|
||||
- 'spec/system/ticket/update/full_quote_header_spec.rb'
|
||||
|
||||
@@ -342,7 +342,7 @@ Metrics/AbcSize:
|
||||
- 'lib/stats/ticket_reopen.rb'
|
||||
- 'lib/stats/ticket_response_time.rb'
|
||||
- 'lib/stats/ticket_waiting_time.rb'
|
||||
- 'lib/telegram.rb'
|
||||
- 'lib/telegram_helper.rb'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'lib/twitter_sync.rb'
|
||||
- 'lib/user_agent.rb'
|
||||
@@ -641,7 +641,7 @@ Metrics/CyclomaticComplexity:
|
||||
- 'lib/stats/ticket_load_measure.rb'
|
||||
- 'lib/stats/ticket_reopen.rb'
|
||||
- 'lib/stats/ticket_waiting_time.rb'
|
||||
- 'lib/telegram.rb'
|
||||
- 'lib/telegram_helper.rb'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'lib/twitter_sync.rb'
|
||||
- 'lib/user_agent.rb'
|
||||
@@ -863,7 +863,7 @@ Metrics/PerceivedComplexity:
|
||||
- 'lib/stats/ticket_reopen.rb'
|
||||
- 'lib/stats/ticket_response_time.rb'
|
||||
- 'lib/stats/ticket_waiting_time.rb'
|
||||
- 'lib/telegram.rb'
|
||||
- 'lib/telegram_helper.rb'
|
||||
- 'lib/transaction_dispatcher.rb'
|
||||
- 'lib/twitter_sync.rb'
|
||||
- 'lib/user_agent.rb'
|
||||
|
||||
182
CHANGELOG.md
182
CHANGELOG.md
@@ -1,11 +1,187 @@
|
||||
# Change Log
|
||||
|
||||
## [5.2.0](https://github.com/zammad/zammad/tree/5.2.0) (2022-xx-xx)
|
||||
## [5.2.3](https://github.com/zammad/zammad/tree/5.2.3) (2022-09-30)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/5.2.2...5.2.3)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Unable to add Microsoft 365 account when admin consent for enterprise applications is enabled [3963](https://github.com/zammad/zammad/issues/3963) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
|
||||
- Zendesk Import: Link to linked Tickets are not working after migration to Zammad [4249](https://github.com/zammad/zammad/issues/4249) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- KB answer linking in ticket zoom fails: Couldn't find KnowledgeBase::Answer with 'id'=315 [4259](https://github.com/zammad/zammad/issues/4259) [[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%20by%20payment)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
|
||||
- Locking a record with unpersisted changes is not supported. Use `save` to persist the changes, or `reload` to discard them explicitly [3664](https://github.com/zammad/zammad/issues/3664) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- HTML table in mail preview is misformatted [3630](https://github.com/zammad/zammad/issues/3630) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Zammad suffers from serious performance degrade on incorrect role configurations [4243](https://github.com/zammad/zammad/issues/4243) [[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%20by%20payment)]
|
||||
- Apply via role granted permission while creating shared draft [4071](https://github.com/zammad/zammad/issues/4071) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
|
||||
- Mandatory field in combination with automatic assignment leads to a divergence in the setting of the ticket owner [4245](https://github.com/zammad/zammad/issues/4245) [[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%20by%20payment)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Type attribute is being ignored on Zendesk import [4246](https://github.com/zammad/zammad/issues/4246) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Email address falsely converted on pasting into article body [3099](https://github.com/zammad/zammad/issues/3099) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Zendesk import does not import comment create date inside of the created articles [4244](https://github.com/zammad/zammad/issues/4244) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Zendesk import does not import embedded images [4234](https://github.com/zammad/zammad/issues/4234) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Fix width of tags add in trigger [4215](https://github.com/zammad/zammad/issues/4215) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
|
||||
- HTML comments/conditions being rendered from emails [4184](https://github.com/zammad/zammad/issues/4184) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
|
||||
- Drag & Drop bulk operations not available in search results [3923](https://github.com/zammad/zammad/issues/3923) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Fixes #3718 - Dashboard stopped working. [4240](https://github.com/zammad/zammad/pull/4240)
|
||||
- Dashboard stopped working [3718](https://github.com/zammad/zammad/issues/3718) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- The webhook could not be saved by Telegram, seems to be an invalid URL. [4156](https://github.com/zammad/zammad/issues/4156) [[bug](https://github.com/zammad/zammad/labels/bug)] [[telegram](https://github.com/zammad/zammad/labels/telegram)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Changing time range in "Wait until" picker at trigger action resets time value to "1" [4207](https://github.com/zammad/zammad/issues/4207) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
|
||||
## [5.2.2](https://github.com/zammad/zammad/tree/5.2.2) (2022-09-13)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/5.2.1...5.2.2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Fixes #849 - Allow additional usage of Ticket Number in (Zoom) URL [4191](https://github.com/zammad/zammad/pull/4191) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[needs verification](https://github.com/zammad/zammad/labels/needs%20verification)]
|
||||
- Allow additional usage of Ticket Number in (Zoom) URL [849](https://github.com/zammad/zammad/issues/849) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend%20%2F%20JS%20app)]
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Alphabetical order of text modules [4221](https://github.com/zammad/zammad/issues/4221) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Core Workflow: Readonly input fields with link template are not clickable [4233](https://github.com/zammad/zammad/issues/4233) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
|
||||
- Tickets can be closed with the bulk action, even when a mandatory field is empty [4198](https://github.com/zammad/zammad/issues/4198) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Article gets shortened when its text is marked with highlighter [4202](https://github.com/zammad/zammad/issues/4202) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Remove "Create new Customer" in trigger [4214](https://github.com/zammad/zammad/issues/4214) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)]
|
||||
- Cannot set date for pending close status in postmaster filter [4206](https://github.com/zammad/zammad/issues/4206) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)]
|
||||
- Article update causes missing icons in the UI after switching internal state [4213](https://github.com/zammad/zammad/issues/4213) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
|
||||
- Fixes #4186 - Multiselect does not show values properly in reports. [4185](https://github.com/zammad/zammad/pull/4185) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Multiselect does not show values properly in reports [4186](https://github.com/zammad/zammad/issues/4186) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Knowledge base formatting issue [4093](https://github.com/zammad/zammad/issues/4093) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
|
||||
- Ticket templates do not save the owner attribute [4175](https://github.com/zammad/zammad/issues/4175) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Knowledge base detail page has long loading times when kb has many categories [4190](https://github.com/zammad/zammad/issues/4190) [[bug](https://github.com/zammad/zammad/labels/bug)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)] [[performance](https://github.com/zammad/zammad/labels/performance)]
|
||||
- Ticket hook not represented consistently inside a ticket with some articles [3861](https://github.com/zammad/zammad/issues/3861) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[good first issue](https://github.com/zammad/zammad/labels/good%20first%20issue)]
|
||||
- multi_tree_select: field is used, but browser says it is empty [4188](https://github.com/zammad/zammad/issues/4188) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Core Workflows no longer allow multiple values for single selection fields [4193](https://github.com/zammad/zammad/issues/4193) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
|
||||
- Unable to remove chat topics if chat sessions exist [4178](https://github.com/zammad/zammad/issues/4178) [[bug](https://github.com/zammad/zammad/labels/bug)] [[chat](https://github.com/zammad/zammad/labels/chat)]
|
||||
- Tree select not proper in Report [4174](https://github.com/zammad/zammad/issues/4174) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Avatar upload fails on cropping image [4171](https://github.com/zammad/zammad/issues/4171) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Unable to scroll ticket image view when using arrow keys [4180](https://github.com/zammad/zammad/issues/4180) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend%20%2F%20JS%20app)]
|
||||
- Fix scrolling image view when using arrow keys [4177](https://github.com/zammad/zammad/pull/4177) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend%20%2F%20JS%20app)]
|
||||
- Secondary organizations are always shown in sidebar even if customer has no secondary organizations assigned [4172](https://github.com/zammad/zammad/issues/4172) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Elasticsearch forgets results from attachments over time [4134](https://github.com/zammad/zammad/issues/4134) [[bug](https://github.com/zammad/zammad/labels/bug)] [[search](https://github.com/zammad/zammad/labels/search)]
|
||||
- Zendesk migration runs randomly in ssl handshake failure triggered from Zendesk site without a retry [4169](https://github.com/zammad/zammad/issues/4169) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Zendesk migration has currently no retry for attachment fetch [4170](https://github.com/zammad/zammad/issues/4170) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Triggers must not allow adding perform settings which are empty / nil [4131](https://github.com/zammad/zammad/issues/4131) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
|
||||
- LDAP tries to sync all the time [4167](https://github.com/zammad/zammad/issues/4167) [[bug](https://github.com/zammad/zammad/labels/bug)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- KB answer image in editor has extra padding [4165](https://github.com/zammad/zammad/issues/4165) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Signatures are taken over from colleagues via "shared draft" function [4042](https://github.com/zammad/zammad/issues/4042) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- MultiSelects do not allow storing values in tickets with MariaDB [4148](https://github.com/zammad/zammad/issues/4148) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)]
|
||||
- Triggers are not executed if article contains a mention and variable ``article.body_as_html`` is used [3991](https://github.com/zammad/zammad/issues/3991) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Attachment from KB answer is still sent if manually removed before update [4149](https://github.com/zammad/zammad/issues/4149) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- User avatars are missing if authentication is done via Google [3829](https://github.com/zammad/zammad/issues/3829) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Displayed ticket counter for overviews with subscribe condition differs from number of shown tickets [3853](https://github.com/zammad/zammad/issues/3853) [[bug](https://github.com/zammad/zammad/labels/bug)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Ticket is not opened, nothing can be viewed if auto assignment selector in admin interface has no value selected [4157](https://github.com/zammad/zammad/issues/4157) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
|
||||
- iCal implementation of Thunderbird can not be handled [3962](https://github.com/zammad/zammad/issues/3962) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
|
||||
## [5.2.1](https://github.com/zammad/zammad/tree/5.2.1) (2022-07-05)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/5.2.0...5.2.1)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Allow very long email address [4159](https://github.com/zammad/zammad/issues/4159) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Elasticsearch 8 incompatible with Zammad 5.2 [4158](https://github.com/zammad/zammad/issues/4158)
|
||||
- Selecting group permission "full" behaves differently in user- and role-management [4100](https://github.com/zammad/zammad/issues/4100) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Trigger Attribute "action > is > updated" is not working after Zammad 5.2 update [4153](https://github.com/zammad/zammad/issues/4153) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- cannot process emails with special characters in local part [4151](https://github.com/zammad/zammad/issues/4151) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- No error message is shown if the mandatory role field is missing during creating/updating overviews [4140](https://github.com/zammad/zammad/issues/4140) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[overviews](https://github.com/zammad/zammad/labels/overviews)]
|
||||
- Tooltips are not displayed correctly for all fields while creating a new group [4129](https://github.com/zammad/zammad/issues/4129) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- LDAP import job no longer starts automatically [4143](https://github.com/zammad/zammad/issues/4143) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- background_services will stop immediately after starting [4141](https://github.com/zammad/zammad/issues/4141) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- The use of shared drafts for response ends with the saving of an internal note [4031](https://github.com/zammad/zammad/issues/4031) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
|
||||
## [5.2.0](https://github.com/zammad/zammad/tree/5.2.0) (2022-06-21)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/5.1.0...5.2.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
|
||||
|
||||
- Time accounting "Overviews" is misleading [4119](https://github.com/zammad/zammad/issues/4119) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[time accounting](https://github.com/zammad/zammad/labels/time%20accounting)]
|
||||
- Translate Slack templates in French [4101](https://github.com/zammad/zammad/pull/4101) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[integration](https://github.com/zammad/zammad/labels/integration)]
|
||||
- Translate ticket merge email templates in French [4099](https://github.com/zammad/zammad/pull/4099) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[translation](https://github.com/zammad/zammad/labels/translation)]
|
||||
- Multi LDAP: Httplog should include source where data was synced from [4106](https://github.com/zammad/zammad/issues/4106) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)]
|
||||
- Multiple LDAP server configurations [3141](https://github.com/zammad/zammad/issues/3141) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- implemented time accounting export by activity [3976](https://github.com/zammad/zammad/pull/3976) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[time accounting](https://github.com/zammad/zammad/labels/time%20accounting)]
|
||||
- no test e-mail [1471](https://github.com/zammad/zammad/issues/1471) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)]
|
||||
- Multi tree select object manager attribute [4062](https://github.com/zammad/zammad/issues/4062) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- parsing incoming mails breaks on this line "Reply-To: <>" [3867](https://github.com/zammad/zammad/issues/3867) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
|
||||
- Prefix the contact form css with 'zammad' for a better selection [4048](https://github.com/zammad/zammad/issues/4048) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[theming / skinning](https://github.com/zammad/zammad/labels/theming%20%2F%20skinning)]
|
||||
- Show age (indicator) for public KB Answers [2848](https://github.com/zammad/zammad/issues/2848) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
|
||||
- Allow reauthentication of Google / Microsoft 365 accounts [3240](https://github.com/zammad/zammad/issues/3240) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[channel](https://github.com/zammad/zammad/labels/channel)]
|
||||
- Avatar lookup during user creation can cause significant delays [3542](https://github.com/zammad/zammad/issues/3542) [[enhancement](https://github.com/zammad/zammad/labels/enhancement)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Zendesk import ignores active flag for user during the import [4135](https://github.com/zammad/zammad/issues/4135) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Zammad shows "<![if !supportLists]>- <![endif]>" instead of list icon [4112](https://github.com/zammad/zammad/issues/4112) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
|
||||
- Zendesk import for users and organizations works not correctly when custom fields are created with a sanitized name [4128](https://github.com/zammad/zammad/issues/4128) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Popovers will not disappear [4122](https://github.com/zammad/zammad/issues/4122) [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Pop-Up Warning for missing attachments with text containing "attached" or related only appears on english terms (not translated) [4096](https://github.com/zammad/zammad/issues/4096) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- 'Sender Format' set to 'Agent Name' causes empty display names in triggers [3890](https://github.com/zammad/zammad/issues/3890) [[bug](https://github.com/zammad/zammad/labels/bug)] [[trigger](https://github.com/zammad/zammad/labels/trigger)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Changing the category of a Knowledge Base answer leads to alternative translation loop [3931](https://github.com/zammad/zammad/issues/3931) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
|
||||
- Switching the photos displayed in the preview does not work with the arrow keys [4030](https://github.com/zammad/zammad/issues/4030) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Webhhok transfers "accounted_time" of an article as integer instead of float/double [4127](https://github.com/zammad/zammad/issues/4127) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- User popover shows secondary organizations label if user has no organizations [4125](https://github.com/zammad/zammad/issues/4125) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- OS detection crashes if OS name cannot be parsed [4124](https://github.com/zammad/zammad/issues/4124) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Upgrade to Zammad 5.2 not possible (undefined method `view' for nil:NilClass) [4126](https://github.com/zammad/zammad/issues/4126) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Notifications are sent for all tickets when an organization of a customer is updated [3952](https://github.com/zammad/zammad/issues/3952) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Single tree select no longer allows displaying child values [4118](https://github.com/zammad/zammad/issues/4118) [[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%2FUI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)]
|
||||
- users customer name is displayed as id [4040](https://github.com/zammad/zammad/issues/4040) [[bug](https://github.com/zammad/zammad/labels/bug)] [[overviews](https://github.com/zammad/zammad/labels/overviews)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Follow-up detection does not work for the body of plain text mails [3894](https://github.com/zammad/zammad/issues/3894) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
|
||||
- DataPrivacy Tasks fail if Zammad has to solve references on users with invalid email addresses [3935](https://github.com/zammad/zammad/issues/3935) [[bug](https://github.com/zammad/zammad/labels/bug)] [[data privacy](https://github.com/zammad/zammad/labels/data%20privacy)]
|
||||
- Parents will always be selected when going through the tree in multiple treeselect field [4080](https://github.com/zammad/zammad/issues/4080) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend%20%2F%20JS%20app)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)] [[form](https://github.com/zammad/zammad/labels/form)]
|
||||
- Title column is missing if ticket number is disabled in overview options [3943](https://github.com/zammad/zammad/issues/3943) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- script/background-services.rb crashes on startup [4113](https://github.com/zammad/zammad/issues/4113) [[bug](https://github.com/zammad/zammad/labels/bug)] [[blocker](https://github.com/zammad/zammad/labels/blocker)]
|
||||
- Monitoring endpoint is complaing about not executed scheduler job "CacheClearJob.perform_now" [4079](https://github.com/zammad/zammad/issues/4079) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Searches display all groups and owners on bulk selections [4054](https://github.com/zammad/zammad/issues/4054) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
|
||||
- Wrong default values in ticket create when creating from user profile [4088](https://github.com/zammad/zammad/issues/4088) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Reporting export file shows empty fields in selected columns when "custom sort option" is enabled [4103](https://github.com/zammad/zammad/issues/4103) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Wrongly creation of "session started" activity stream log entry for session check [4105](https://github.com/zammad/zammad/issues/4105) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Latest upgrade to 5.1 is not possible [4097](https://github.com/zammad/zammad/issues/4097) [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Trigger autocomplete when clicking the "Tags" field also without a search term [4095](https://github.com/zammad/zammad/issues/4095) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Shared draft attribute in object manage not disabled for onclick [4089](https://github.com/zammad/zammad/issues/4089) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)]
|
||||
- Assign user to multiple organizations [1573](https://github.com/zammad/zammad/issues/1573) [[feature backlog](https://github.com/zammad/zammad/labels/feature%20backlog)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Preview of *.ics files in ticket zoom attachments [4091](https://github.com/zammad/zammad/issues/4091) [[feature backlog](https://github.com/zammad/zammad/labels/feature%20backlog)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[productivity](https://github.com/zammad/zammad/labels/productivity)]
|
||||
- Tab behaviour does not show properly when doing a ticket update at the same time [4094](https://github.com/zammad/zammad/issues/4094) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Custom sort on select returns "undefined method `merge' " for older objects [4086](https://github.com/zammad/zammad/issues/4086) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)]
|
||||
- Tree Select Trigger changes the conditions on its own [4090](https://github.com/zammad/zammad/issues/4090) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Zammad email display broken [4053](https://github.com/zammad/zammad/issues/4053) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Linked ticket state won't be updated [476](https://github.com/zammad/zammad/issues/476) [[bug](https://github.com/zammad/zammad/labels/bug)] [[frontend / JS app](https://github.com/zammad/zammad/labels/frontend%20%2F%20JS%20app)]
|
||||
- OTRS migration currently do not support custom dropdown multi-select fields [4057](https://github.com/zammad/zammad/issues/4057) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
- OTRS import fails for DynamicFields of Type Dropdown without possible values [2375](https://github.com/zammad/zammad/issues/2375) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
- Boolean shows wrong default value if not set to shown by default [4065](https://github.com/zammad/zammad/issues/4065) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
|
||||
- Zammad notification emails have a broken 'reason' footer line [4066](https://github.com/zammad/zammad/issues/4066) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Javascript error undefined multiple while opening core workflow with tree selects [4067](https://github.com/zammad/zammad/issues/4067) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Popovers will not disappear anymore [4058](https://github.com/zammad/zammad/issues/4058) [[bug](https://github.com/zammad/zammad/labels/bug)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- Freshdesk import currently doesn't handle the ticket type field [4064](https://github.com/zammad/zammad/issues/4064) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
- Core Workflow: Does show wrong field list if you only have admin permissions and not ticket.agent permissions [4035](https://github.com/zammad/zammad/issues/4035) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Migrations (Zendesk, Kayako) currently do not support custom dropdown multi-select fields [4055](https://github.com/zammad/zammad/issues/4055) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Ticket in the ticket creation screen tags are shown after 2 characters, in the ticket zoom after 1 character [3748](https://github.com/zammad/zammad/issues/3748) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Clicking the button in a mail opens the link as a photo [4044](https://github.com/zammad/zammad/issues/4044) [[bug](https://github.com/zammad/zammad/labels/bug)] [[ticket](https://github.com/zammad/zammad/labels/ticket)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Tree select widget closes unexpectedly [4041](https://github.com/zammad/zammad/issues/4041) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- pre-code tags cause Zammad to swallow content [4039](https://github.com/zammad/zammad/issues/4039) [[bug](https://github.com/zammad/zammad/labels/bug)] [[mail processing](https://github.com/zammad/zammad/labels/mail%20processing)]
|
||||
- Error on second migration of an microsoft365 channel undefined method `options' for nil:NilClass [4060](https://github.com/zammad/zammad/issues/4060) [[bug](https://github.com/zammad/zammad/labels/bug)] [[channel](https://github.com/zammad/zammad/labels/channel)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Freshdesk migration which doesn't support time entries runs in unwanted behaviour [4052](https://github.com/zammad/zammad/issues/4052) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
- Freshdesk-Migration does not like no Companies / Organisations within the source [4051](https://github.com/zammad/zammad/issues/4051) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)]
|
||||
- Cannot encrypt if multiple S/MIME certificates exist and one is expired [4029](https://github.com/zammad/zammad/issues/4029) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Can't delete smime certificate - ERROR: duplicate key value violates unique constraint [4049](https://github.com/zammad/zammad/issues/4049) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Core Workflow - Unable to select "has changed" logic when creating new workflow. It is also replaced with "is" in previously created workflows. [4046](https://github.com/zammad/zammad/issues/4046) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- undefined method `to_hash` on editing select fields in the admin interface after migration to 5.1 [4027](https://github.com/zammad/zammad/issues/4027) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Knowledge Base on-the-fly updating does not work [4026](https://github.com/zammad/zammad/issues/4026) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Default time diff for date and datetime objects don't function [3763](https://github.com/zammad/zammad/issues/3763) [[bug](https://github.com/zammad/zammad/labels/bug)] [[object manager attribute](https://github.com/zammad/zammad/labels/object%20manager%20attribute)]
|
||||
- Problems with duplicate records in Translations table [4020](https://github.com/zammad/zammad/issues/4020) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)]
|
||||
- Tree select value cannot be set to "-" (empty) with Trigger/Scheduler/Core workflow [4024](https://github.com/zammad/zammad/issues/4024) [[bug](https://github.com/zammad/zammad/labels/bug)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Admin macros are not usable with admin permissions only [4032](https://github.com/zammad/zammad/issues/4032) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Admin SLA index view does not show update time if "for agent respond" is selected [4033](https://github.com/zammad/zammad/issues/4033) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Core Workflow: Stop after match is not saved [4034](https://github.com/zammad/zammad/issues/4034) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)] [[core workflows](https://github.com/zammad/zammad/labels/core%20workflows)]
|
||||
- KB Public menus scroll bar positioning incorrect [4008](https://github.com/zammad/zammad/issues/4008) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[knowledge base](https://github.com/zammad/zammad/labels/knowledge%20base)]
|
||||
- Freshdesk import error when a inline image source attribute contains no clean url [4028](https://github.com/zammad/zammad/issues/4028) [[bug](https://github.com/zammad/zammad/labels/bug)] [[import](https://github.com/zammad/zammad/labels/import)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- Mention history does not show properly who got added [3984](https://github.com/zammad/zammad/issues/3984) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[history](https://github.com/zammad/zammad/labels/history)]
|
||||
- Translation introduction text shows wrong keyboard shortcut [2885](https://github.com/zammad/zammad/issues/2885) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[translation](https://github.com/zammad/zammad/labels/translation)]
|
||||
- Translation introduction text shows wrong keyboard shortcut [3990](https://github.com/zammad/zammad/pull/3990) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Obsolete notice field in admin calendar view [4023](https://github.com/zammad/zammad/issues/4023) [[bug](https://github.com/zammad/zammad/labels/bug)] [[admin area](https://github.com/zammad/zammad/labels/admin%20area)]
|
||||
- Error “customer_id required” on Macro execution [4022](https://github.com/zammad/zammad/issues/4022) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[macros](https://github.com/zammad/zammad/labels/macros)]
|
||||
- Translations are not available in API after syncing [4016](https://github.com/zammad/zammad/issues/4016) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Upgraded to 5.1.0: pending time doesn’t show a date [4018](https://github.com/zammad/zammad/issues/4018) [[bug](https://github.com/zammad/zammad/labels/bug)] [[translation](https://github.com/zammad/zammad/labels/translation)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
- UI is not updated right after importing users csv file [3919](https://github.com/zammad/zammad/issues/3919) [[bug](https://github.com/zammad/zammad/labels/bug)]
|
||||
- Pending closed/reminder date selector shows current date black on dark gray [2316](https://github.com/zammad/zammad/issues/2316) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[regression](https://github.com/zammad/zammad/labels/regression)]
|
||||
- LDAP with many groups (<5k) and group role relation (>50) will crash in frontend [3994](https://github.com/zammad/zammad/issues/3994) [[bug](https://github.com/zammad/zammad/labels/bug)] [[UX/UI](https://github.com/zammad/zammad/labels/UX%2FUI)] [[LDAP](https://github.com/zammad/zammad/labels/LDAP)] [[prioritised by payment](https://github.com/zammad/zammad/labels/prioritised%20by%20payment)]
|
||||
|
||||
7
Gemfile
7
Gemfile
@@ -103,8 +103,7 @@ gem 'rack-attack'
|
||||
# channels
|
||||
gem 'gmail_xoauth'
|
||||
gem 'koala'
|
||||
# TODO: remove git information after https://github.com/bennesp/telegramAPI/pull/8 is merged
|
||||
gem 'telegramAPI', git: 'https://github.com/zammad-deps/telegramAPI', branch: 'uri-escape-warning'
|
||||
gem 'telegram-bot-ruby'
|
||||
gem 'twitter'
|
||||
|
||||
# channels - email additions
|
||||
@@ -233,4 +232,6 @@ end
|
||||
# ZAMMAD DEVS: Consult the internal wiki
|
||||
# (or else risk pushing unwanted changes to Gemfile.lock!)
|
||||
# https://git.zammad.com/zammad/zammad/wikis/Tips#user-content-customizing-the-gemfile
|
||||
eval_gemfile 'Gemfile.local' if File.exist?('Gemfile.local')
|
||||
Dir['Gemfile.local*'].each do |file|
|
||||
eval_gemfile file
|
||||
end
|
||||
|
||||
163
Gemfile.lock
163
Gemfile.lock
@@ -16,14 +16,6 @@ GIT
|
||||
mail (2.7.2.edge)
|
||||
mini_mime (>= 0.1.1)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/zammad-deps/telegramAPI
|
||||
revision: fb62dd5b48b0c41169ab69fc89069969ab4705b2
|
||||
branch: uri-escape-warning
|
||||
specs:
|
||||
telegramAPI (1.4)
|
||||
rest-client (~> 2.0, >= 2.0.2)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
@@ -31,52 +23,52 @@ GEM
|
||||
simple_po_parser (~> 1.1.6)
|
||||
aasm (5.2.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
actioncable (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actioncable (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activejob (= 6.1.6)
|
||||
activerecord (= 6.1.6)
|
||||
activestorage (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionmailbox (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activestorage (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
actionview (= 6.1.6)
|
||||
activejob (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionmailer (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
actionview (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.1.6)
|
||||
actionview (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionpack (6.1.7)
|
||||
actionview (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
rack (~> 2.0, >= 2.0.9)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activerecord (= 6.1.6)
|
||||
activestorage (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actiontext (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activestorage (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
actionview (6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
activejob (6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
activerecord (6.1.6)
|
||||
activemodel (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
activemodel (6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
activerecord (6.1.7)
|
||||
activemodel (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
activerecord-import (1.4.0)
|
||||
activerecord (>= 4.2)
|
||||
activerecord-nulldb-adapter (0.8.0)
|
||||
@@ -87,14 +79,14 @@ GEM
|
||||
multi_json (~> 1.11, >= 1.11.2)
|
||||
rack (>= 2.0.8, < 3)
|
||||
railties (>= 5.2.4.1)
|
||||
activestorage (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activejob (= 6.1.6)
|
||||
activerecord (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
activestorage (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (6.1.6)
|
||||
activesupport (6.1.7)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -110,6 +102,10 @@ GEM
|
||||
ast (2.4.2)
|
||||
autoprefixer-rails (10.4.7.0)
|
||||
execjs (~> 2)
|
||||
axiom-types (0.1.1)
|
||||
descendants_tracker (~> 0.0.4)
|
||||
ice_nine (~> 0.11.0)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
binding_of_caller (1.0.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
biz (1.8.2)
|
||||
@@ -137,6 +133,8 @@ GEM
|
||||
clearbit (0.3.3)
|
||||
nestful (~> 1.1.0)
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
coffee-rails (5.0.0)
|
||||
coffee-script (>= 2.2.0)
|
||||
railties (>= 5.2.0)
|
||||
@@ -161,6 +159,8 @@ GEM
|
||||
delayed_job (>= 3.0, < 5)
|
||||
deprecation_toolkit (2.0.0)
|
||||
activesupport (>= 5.2)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
diff-lcs (1.5.0)
|
||||
diffy (3.4.1)
|
||||
domain_name (0.5.20190701)
|
||||
@@ -169,6 +169,7 @@ GEM
|
||||
railties (>= 5)
|
||||
dotenv (2.7.6)
|
||||
dry-cli (0.7.0)
|
||||
dry-inflector (0.3.0)
|
||||
eco (1.0.0)
|
||||
coffee-script
|
||||
eco-source
|
||||
@@ -180,7 +181,7 @@ GEM
|
||||
email_address (0.2.3)
|
||||
simpleidn
|
||||
equalizer (0.0.11)
|
||||
erubi (1.10.0)
|
||||
erubi (1.11.0)
|
||||
eventmachine (1.2.7)
|
||||
execjs (2.8.1)
|
||||
factory_bot (6.2.1)
|
||||
@@ -237,7 +238,6 @@ GEM
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 2.2)
|
||||
http-parser (~> 1.2.0)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.5)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
@@ -245,7 +245,7 @@ GEM
|
||||
ffi-compiler (>= 1.0, < 2.0)
|
||||
http_parser.rb (0.6.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.10.0)
|
||||
i18n (1.12.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.7.1)
|
||||
ice_cube (~> 0.16)
|
||||
@@ -253,6 +253,7 @@ GEM
|
||||
icalendar (~> 2.0)
|
||||
ice_cube (~> 0.16)
|
||||
ice_cube (0.16.4)
|
||||
ice_nine (0.11.2)
|
||||
inflection (1.0.0)
|
||||
iniparse (1.5.0)
|
||||
interception (0.5)
|
||||
@@ -271,7 +272,7 @@ GEM
|
||||
logging (2.3.1)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.14)
|
||||
loofah (2.18.0)
|
||||
loofah (2.19.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
marcel (1.0.2)
|
||||
@@ -286,7 +287,7 @@ GEM
|
||||
mime-types-data (3.2022.0105)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.8.0)
|
||||
minitest (5.16.0)
|
||||
minitest (5.16.3)
|
||||
msgpack (1.5.2)
|
||||
multi_json (1.15.0)
|
||||
multi_xml (0.6.0)
|
||||
@@ -295,9 +296,8 @@ GEM
|
||||
naught (1.1.0)
|
||||
nestful (1.1.4)
|
||||
net-ldap (0.17.1)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.13.6)
|
||||
nokogiri (1.13.8)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nori (2.6.0)
|
||||
@@ -308,7 +308,7 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
omniauth (1.9.1)
|
||||
omniauth (1.9.2)
|
||||
hashie (>= 3.4.6)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-facebook (9.0.0)
|
||||
@@ -380,27 +380,27 @@ GEM
|
||||
pundit-matchers (1.7.0)
|
||||
rspec-rails (>= 3.0.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3.1)
|
||||
rack (2.2.4)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-proxy (0.7.2)
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.1.6)
|
||||
actioncable (= 6.1.6)
|
||||
actionmailbox (= 6.1.6)
|
||||
actionmailer (= 6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
actiontext (= 6.1.6)
|
||||
actionview (= 6.1.6)
|
||||
activejob (= 6.1.6)
|
||||
activemodel (= 6.1.6)
|
||||
activerecord (= 6.1.6)
|
||||
activestorage (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
rack-test (2.0.2)
|
||||
rack (>= 1.3)
|
||||
rails (6.1.7)
|
||||
actioncable (= 6.1.7)
|
||||
actionmailbox (= 6.1.7)
|
||||
actionmailer (= 6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
actiontext (= 6.1.7)
|
||||
actionview (= 6.1.7)
|
||||
activejob (= 6.1.7)
|
||||
activemodel (= 6.1.7)
|
||||
activerecord (= 6.1.7)
|
||||
activestorage (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 6.1.6)
|
||||
railties (= 6.1.7)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
@@ -411,9 +411,9 @@ GEM
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.3)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.6)
|
||||
actionpack (= 6.1.6)
|
||||
activesupport (= 6.1.6)
|
||||
railties (6.1.7)
|
||||
actionpack (= 6.1.7)
|
||||
activesupport (= 6.1.7)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
@@ -426,11 +426,6 @@ GEM
|
||||
rchardet (1.8.0)
|
||||
redis (4.6.0)
|
||||
regexp_parser (2.5.0)
|
||||
rest-client (2.1.0)
|
||||
http-accept (>= 1.7.0, < 2.0)
|
||||
http-cookie (>= 1.0.2, < 2.0)
|
||||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
rexml (3.2.5)
|
||||
rspec-core (3.11.0)
|
||||
rspec-support (~> 3.11.0)
|
||||
@@ -496,7 +491,7 @@ GEM
|
||||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
selenium-webdriver (4.2.1)
|
||||
selenium-webdriver (4.3.0)
|
||||
childprocess (>= 0.5, < 5.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
@@ -524,6 +519,10 @@ GEM
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
tcr (0.2.2)
|
||||
telegram-bot-ruby (0.19.2)
|
||||
dry-inflector
|
||||
faraday (~> 1.0)
|
||||
virtus (~> 2.0)
|
||||
telephone_number (1.4.16)
|
||||
test-unit (3.5.3)
|
||||
power_assert
|
||||
@@ -545,7 +544,7 @@ GEM
|
||||
multipart-post (~> 2.0)
|
||||
naught (~> 1.0)
|
||||
simple_oauth (~> 0.3.0)
|
||||
tzinfo (2.0.4)
|
||||
tzinfo (2.0.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
@@ -562,6 +561,10 @@ GEM
|
||||
logging
|
||||
nokogiri
|
||||
rubyntlm
|
||||
virtus (2.0.0)
|
||||
axiom-types (~> 0.1)
|
||||
coercible (~> 1.0)
|
||||
descendants_tracker (~> 0.0, >= 0.0.3)
|
||||
vite_rails (3.0.10)
|
||||
railties (>= 5.1, < 8)
|
||||
vite_ruby (~> 3.0)
|
||||
@@ -685,7 +688,7 @@ DEPENDENCIES
|
||||
spring
|
||||
sprockets (~> 3.7.2)
|
||||
tcr
|
||||
telegramAPI!
|
||||
telegram-bot-ruby
|
||||
telephone_number
|
||||
test-unit
|
||||
thor
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
-----------------------------------------------------------------------------
|
||||
ba-linkify.js
|
||||
Source: http://benalman.com/code/test/js-linkify/
|
||||
Copyright: 2009 "Cowboy" Ben Alman
|
||||
License: MIT license
|
||||
-----------------------------------------------------------------------------
|
||||
bootstrap
|
||||
Source: https://github.com/twbs/bootstrap
|
||||
Copyright: 2011-2015 Twitter, Inc.
|
||||
@@ -107,6 +102,11 @@ Source: https://github.com/customd/jquery-visible
|
||||
Copyright: 2012, Digital Fusion
|
||||
License: MIT license
|
||||
-----------------------------------------------------------------------------
|
||||
linkify.js
|
||||
Source: https://github.com/Hypercontext/linkifyjs
|
||||
Copyright: Copyright (c) 2021 SoapBox Innovations Inc.
|
||||
License: MIT license
|
||||
-----------------------------------------------------------------------------
|
||||
marked.js
|
||||
Source: https://github.com/chjj/marked
|
||||
Copyright: 2011-2014, Christopher Jeffrey
|
||||
|
||||
@@ -784,7 +784,8 @@ class App.ControllerForm extends App.Controller
|
||||
# set autofocus by delay to make validation testable
|
||||
App.Delay.set(
|
||||
->
|
||||
lookupForm.find('.has-error').find('input, textarea, select').first().trigger('focus')
|
||||
# make sure to work for all field types (e.g. column_select which is an hidden select)
|
||||
lookupForm.find('.has-error').get(0)?.scrollIntoView(true)
|
||||
200
|
||||
'validate'
|
||||
)
|
||||
|
||||
@@ -36,6 +36,7 @@ class ChannelAccountOverview extends App.ControllerSubContent
|
||||
requiredPermission: 'admin.channel_microsoft365'
|
||||
events:
|
||||
'click .js-new': 'new'
|
||||
'click .js-admin-consent': 'adminConsent'
|
||||
'click .js-delete': 'delete'
|
||||
'click .js-reauthenticate': 'reauthenticate'
|
||||
'click .js-configApp': 'configApp'
|
||||
@@ -126,6 +127,10 @@ class ChannelAccountOverview extends App.ControllerSubContent
|
||||
@editInbound(undefined, @channel_id, true)
|
||||
@channel_id = undefined
|
||||
|
||||
if @error_code is 'AADSTS65004'
|
||||
@error_code = undefined
|
||||
new AdminConsentInfo(container: @container)
|
||||
|
||||
show: (params) =>
|
||||
for key, value of params
|
||||
if key isnt 'el' && key isnt 'shown' && key isnt 'match'
|
||||
@@ -141,6 +146,9 @@ class ChannelAccountOverview extends App.ControllerSubContent
|
||||
new: (e) ->
|
||||
window.location.href = "#{@apiPath}/external_credentials/microsoft365/link_account"
|
||||
|
||||
adminConsent: (e) ->
|
||||
window.location.href = "#{@apiPath}/external_credentials/microsoft365/link_account?prompt=consent"
|
||||
|
||||
delete: (e) =>
|
||||
e.preventDefault()
|
||||
id = $(e.target).closest('.action').data('id')
|
||||
@@ -428,4 +436,16 @@ class AppConfig extends App.ControllerModal
|
||||
@el.find('.alert').removeClass('hidden').text(data.error || __('App could not be verified.'))
|
||||
)
|
||||
|
||||
class AdminConsentInfo extends App.ControllerModal
|
||||
buttonClose: true
|
||||
small: true
|
||||
buttonSubmit: __('Close')
|
||||
head: __('Admin Consent')
|
||||
|
||||
content: ->
|
||||
App.view('microsoft365/admin_consent')()
|
||||
|
||||
onSubmit: =>
|
||||
@close()
|
||||
|
||||
App.Config.set('microsoft365', { prio: 5000, name: __('Microsoft 365'), parent: '#channels', target: '#channels/microsoft365', controller: App.ChannelMicrosoft365, permission: ['admin.channel_microsoft365'] }, 'NavBarAdmin')
|
||||
|
||||
@@ -0,0 +1,598 @@
|
||||
# coffeelint: disable=camel_case_classes
|
||||
|
||||
###
|
||||
|
||||
UI Element options:
|
||||
|
||||
**attribute.notification**
|
||||
|
||||
- Allows to send notifications (default: false)
|
||||
|
||||
**attribute.ticket_delete**
|
||||
|
||||
- Allows to delete the ticket (default: false)
|
||||
|
||||
**attribute.user_action**
|
||||
|
||||
- Allows pre conditions like current_user.id or user session specific values (default: true)
|
||||
|
||||
###
|
||||
|
||||
class App.UiElement.ApplicationAction
|
||||
@defaults: (attribute) ->
|
||||
defaults = ['ticket.state_id']
|
||||
|
||||
groups =
|
||||
ticket:
|
||||
name: __('Ticket')
|
||||
model: 'Ticket'
|
||||
article:
|
||||
name: __('Article')
|
||||
model: 'Article'
|
||||
|
||||
if attribute.notification
|
||||
groups.notification =
|
||||
name: __('Notification')
|
||||
model: 'Notification'
|
||||
|
||||
# merge config
|
||||
elements = {}
|
||||
for groupKey, groupMeta of groups
|
||||
if !groupMeta.model || !App[groupMeta.model]
|
||||
if groupKey is 'notification'
|
||||
elements["#{groupKey}.email"] = { name: 'email', display: __('Email') }
|
||||
elements["#{groupKey}.sms"] = { name: 'sms', display: __('SMS') }
|
||||
elements["#{groupKey}.webhook"] = { name: 'webhook', display: __('Webhook') }
|
||||
else if groupKey is 'article'
|
||||
elements["#{groupKey}.note"] = { name: 'note', display: __('Note') }
|
||||
else
|
||||
|
||||
for row in App[groupMeta.model].configure_attributes
|
||||
|
||||
# ignore passwords and relations
|
||||
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
|
||||
|
||||
# ignore readonly attributes
|
||||
if !row.readonly
|
||||
config = _.clone(row)
|
||||
|
||||
switch config.tag
|
||||
when 'datetime'
|
||||
config.operator = ['static', 'relative']
|
||||
when 'tag'
|
||||
config.operator = ['add', 'remove']
|
||||
|
||||
elements["#{groupKey}.#{config.name}"] = config
|
||||
|
||||
# add ticket deletion action
|
||||
if attribute.ticket_delete
|
||||
elements['ticket.action'] =
|
||||
name: 'action'
|
||||
display: __('Action')
|
||||
tag: 'select'
|
||||
null: false
|
||||
translate: true
|
||||
options:
|
||||
delete: 'Delete'
|
||||
|
||||
[defaults, groups, elements]
|
||||
|
||||
@placeholder: (elementFull, attribute, params, groups, elements) ->
|
||||
item = $( App.view('generic/ticket_perform_action/row')( attribute: attribute ) )
|
||||
selector = @buildAttributeSelector(elementFull, groups, elements)
|
||||
item.find('.js-attributeSelector').prepend(selector)
|
||||
item
|
||||
|
||||
@render: (attribute, params = {}) ->
|
||||
|
||||
[defaults, groups, elements] = @defaults(attribute)
|
||||
|
||||
# return item
|
||||
item = $( App.view('generic/ticket_perform_action/index')( attribute: attribute ) )
|
||||
|
||||
# add filter
|
||||
item.on('click', '.js-rowActions .js-add', (e) =>
|
||||
element = $(e.target).closest('.js-filterElement')
|
||||
placeholder = @placeholder(item, attribute, params, groups, elements)
|
||||
if element.get(0)
|
||||
element.after(placeholder)
|
||||
else
|
||||
item.append(placeholder)
|
||||
placeholder.find('.js-attributeSelector select').trigger('change')
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# remove filter
|
||||
item.on('click', '.js-rowActions .js-remove', (e) =>
|
||||
return if $(e.currentTarget).hasClass('is-disabled')
|
||||
$(e.target).closest('.js-filterElement').remove()
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change attribute selector
|
||||
item.on('change', '.js-attributeSelector select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change operator selector
|
||||
item.on('change', '.js-operator select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
)
|
||||
|
||||
# build initial params
|
||||
if _.isEmpty(params[attribute.name])
|
||||
|
||||
for groupAndAttribute in defaults
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
item.append(element)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, {}, attribute)
|
||||
|
||||
else
|
||||
|
||||
for groupAndAttribute, meta of params[attribute.name]
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, meta, attribute)
|
||||
item.append(element)
|
||||
|
||||
@disableRemoveForOneAttribute(item)
|
||||
item
|
||||
|
||||
@elementKeyGroup: (elementKey) ->
|
||||
elementKey.split(/\./)[0]
|
||||
|
||||
@buildAttributeSelector: (elementFull, groups, elements) ->
|
||||
|
||||
# find first possible attribute
|
||||
selectedValue = ''
|
||||
elementFull.find('.js-attributeSelector select option').each(->
|
||||
if !selectedValue && !$(@).prop('disabled')
|
||||
selectedValue = $(@).val()
|
||||
)
|
||||
|
||||
selection = $('<select class="form-control"></select>')
|
||||
for groupKey, groupMeta of groups
|
||||
displayName = App.i18n.translateInline(groupMeta.name)
|
||||
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
|
||||
optgroup = selection.find("optgroup.js-#{groupKey}")
|
||||
for elementKey, elementGroup of elements
|
||||
elementGroup = @elementKeyGroup(elementKey)
|
||||
if elementGroup is groupKey
|
||||
attributeConfig = elements[elementKey]
|
||||
displayName = App.i18n.translateInline(attributeConfig.display)
|
||||
|
||||
selected = ''
|
||||
if elementKey is selectedValue
|
||||
selected = 'selected="selected"'
|
||||
optgroup.append("<option value=\"#{elementKey}\" #{selected}>#{displayName}</option>")
|
||||
selection
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute: (elementFull) ->
|
||||
if elementFull.find('.js-attributeSelector select').length > 1
|
||||
elementFull.find('.js-remove').removeClass('is-disabled')
|
||||
else
|
||||
elementFull.find('.js-remove').addClass('is-disabled')
|
||||
|
||||
@updateAttributeSelectors: (elementFull) ->
|
||||
|
||||
# enable all
|
||||
elementFull.find('.js-attributeSelector select option').prop('disabled', false)
|
||||
|
||||
# disable all used attributes
|
||||
elementFull.find('.js-attributeSelector select').each(->
|
||||
keyLocal = $(@).val()
|
||||
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
|
||||
)
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute(elementFull)
|
||||
|
||||
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
# set attribute
|
||||
if groupAndAttribute
|
||||
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
|
||||
|
||||
notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
|
||||
articleTypeMatch = groupAndAttribute.match(/^article.([\w]+)$/)
|
||||
|
||||
if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
@buildNotificationArea(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else if _.isArray(articleTypeMatch) && articleType = articleTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
@buildArticleArea(articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
if !elementRow.find('.js-setAttribute div').get(0)
|
||||
attributeSelectorElement = $( App.view('generic/ticket_perform_action/attribute_selector')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
meta: meta || {}
|
||||
))
|
||||
elementRow.find('.js-setAttribute').html(attributeSelectorElement).removeClass('hide')
|
||||
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
|
||||
if !meta.operator
|
||||
meta.operator = currentOperator
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::operator"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
if !attributeConfig || !attributeConfig.operator
|
||||
elementRow.find('.js-operator').parent().addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-operator').parent().removeClass('hide')
|
||||
if attributeConfig && attributeConfig.operator
|
||||
for operator in attributeConfig.operator
|
||||
operatorName = App.i18n.translateInline(operator)
|
||||
selected = ''
|
||||
if meta.operator is operator
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
|
||||
selection
|
||||
|
||||
elementRow.find('.js-operator select').replaceWith(selection)
|
||||
|
||||
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
|
||||
if !meta.pre_condition
|
||||
meta.pre_condition = currentPreCondition
|
||||
|
||||
toggleValue = =>
|
||||
preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
if preCondition isnt 'specific'
|
||||
elementRow.find('.js-value select').html('')
|
||||
elementRow.find('.js-value').addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-value').removeClass('hide')
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
# force to use auto complition on user lookup
|
||||
attribute = clone(attributeConfig, true)
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
attributeSelected = elements[groupAndAttribute]
|
||||
|
||||
preCondition = false
|
||||
if attributeSelected.relation is 'User'
|
||||
preCondition = 'user'
|
||||
attribute.tag = 'user_autocompletion'
|
||||
if attributeSelected.relation is 'Organization'
|
||||
preCondition = 'org'
|
||||
attribute.tag = 'autocompletion_ajax'
|
||||
if !preCondition || attribute.user_action is false
|
||||
elementRow.find('.js-preCondition select').html('')
|
||||
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
|
||||
toggleValue()
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
return
|
||||
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
|
||||
options = {}
|
||||
if preCondition is 'user'
|
||||
options =
|
||||
'current_user.id': App.i18n.translateInline('current user')
|
||||
'specific': App.i18n.translateInline('specific user')
|
||||
|
||||
if attributeSelected.null is true
|
||||
options['not_set'] = App.i18n.translateInline('unassign user')
|
||||
|
||||
else if preCondition is 'org'
|
||||
options =
|
||||
'current_user.organization_id': App.i18n.translateInline('current user organization')
|
||||
'specific': App.i18n.translateInline('specific organization')
|
||||
|
||||
for key, value of options
|
||||
selected = ''
|
||||
if key is meta.pre_condition
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
elementRow.find('.js-preCondition select').replaceWith(selection)
|
||||
|
||||
elementRow.find('.js-preCondition select').on('change', (e) ->
|
||||
toggleValue()
|
||||
)
|
||||
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
toggleValue()
|
||||
|
||||
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
|
||||
# build new item
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
config = clone(attributeConfig, true)
|
||||
|
||||
if config.relation is 'User'
|
||||
config.tag = 'user_autocompletion'
|
||||
config.disableCreateObject = true
|
||||
if config.relation is 'Organization'
|
||||
config.tag = 'autocompletion_ajax'
|
||||
|
||||
# render ui element
|
||||
item = ''
|
||||
if config && App.UiElement[config.tag]
|
||||
config['name'] = name
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
|
||||
config.multiple = false
|
||||
config.default = undefined
|
||||
config.nulloption = config.null
|
||||
if config.tag is 'multiselect' || config.tag is 'multi_tree_select'
|
||||
config.multiple = true
|
||||
if config.tag is 'checkbox'
|
||||
config.tag = 'select'
|
||||
if config.tag is 'datetime'
|
||||
config.validationContainer = 'self'
|
||||
item = App.UiElement[config.tag].render(config, {})
|
||||
|
||||
relative_operators = [
|
||||
__('before (relative)'),
|
||||
__('within next (relative)'),
|
||||
__('within last (relative)'),
|
||||
__('after (relative)'),
|
||||
__('till (relative)'),
|
||||
__('from (relative)'),
|
||||
__('relative'),
|
||||
]
|
||||
|
||||
upcoming_operator = meta.operator
|
||||
|
||||
if !_.include(config.operator, upcoming_operator)
|
||||
if Array.isArray(config.operator)
|
||||
upcoming_operator = config.operator[0]
|
||||
else
|
||||
upcoming_operator = null
|
||||
|
||||
if _.include(relative_operators, upcoming_operator)
|
||||
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute])
|
||||
item = App.UiElement['time_range'].render(config, {})
|
||||
|
||||
elementRow.find('.js-setAttribute > .flex > .js-value').removeClass('hide').html(item)
|
||||
|
||||
@buildNotificationArea: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
|
||||
|
||||
elementRow.find('.js-setNotification').empty()
|
||||
|
||||
options =
|
||||
'article_last_sender': __('Sender of last article')
|
||||
'ticket_owner': __('Owner')
|
||||
'ticket_customer': __('Customer')
|
||||
'ticket_agents': __('All agents')
|
||||
|
||||
name = "#{attribute.name}::notification.#{notificationType}"
|
||||
|
||||
messageLength = switch notificationType
|
||||
when 'sms' then 160
|
||||
else 200000
|
||||
|
||||
# meta.recipient was a string in the past (single-select) so we convert it to array if needed
|
||||
if !_.isArray(meta.recipient)
|
||||
meta.recipient = [meta.recipient]
|
||||
|
||||
columnSelectOptions = []
|
||||
for key, value of options
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectOptions.push({ value: key, name: App.i18n.translatePlain(value), selected: selected })
|
||||
|
||||
columnSelectRecipientUserOptions = []
|
||||
for user in App.User.all()
|
||||
key = "userid_#{user.id}"
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectRecipientUserOptions.push({ value: key, name: "#{user.firstname} #{user.lastname}", selected: selected })
|
||||
|
||||
columnSelectRecipient = new App.ColumnSelect
|
||||
attribute:
|
||||
name: "#{name}::recipient"
|
||||
options: [
|
||||
{
|
||||
label: __('Variables'),
|
||||
group: columnSelectOptions
|
||||
},
|
||||
{
|
||||
label: __('User'),
|
||||
group: columnSelectRecipientUserOptions
|
||||
},
|
||||
]
|
||||
|
||||
selectionRecipient = columnSelectRecipient.element()
|
||||
|
||||
if notificationType is 'webhook'
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/webhook')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
|
||||
if App.Webhook.search(filter: { active: true }).length isnt 0 || !_.isEmpty(meta.webhook_id)
|
||||
webhookSelection = App.UiElement.select.render(
|
||||
name: "#{name}::webhook_id"
|
||||
multiple: false
|
||||
null: false
|
||||
relation: 'Webhook'
|
||||
value: meta.webhook_id
|
||||
translate: false
|
||||
nulloption: true
|
||||
)
|
||||
else
|
||||
webhookSelection = App.view('generic/ticket_perform_action/webhook_not_available')( attribute: attribute )
|
||||
|
||||
notificationElement.find('.js-webhooks').html(webhookSelection)
|
||||
|
||||
else
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/notification')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
visibilitySelection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: __('internal'), false: __('public') }
|
||||
value: meta.internal || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
includeAttachmentsCheckbox = App.UiElement.select.render(
|
||||
name: "#{name}::include_attachments"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: __('Yes'), false: __('No') }
|
||||
value: meta.include_attachments || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
notificationElement.find('.js-internal').html(visibilitySelection)
|
||||
notificationElement.find('.js-include_attachments').html(includeAttachmentsCheckbox)
|
||||
|
||||
notificationElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: __('message')
|
||||
maxlength: messageLength
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: __('Ticket')
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: __('Article')
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: __('Current User')
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide')
|
||||
|
||||
if App.Config.get('smime_integration') == true
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::sign"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': __('Do not sign email')
|
||||
'discard': __('Sign email (if not possible, discard notification)')
|
||||
'always': __('Sign email (if not possible, send notification anyway)')
|
||||
}
|
||||
value: meta.sign
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-sign').html(selection)
|
||||
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::encryption"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': __('Do not encrypt email')
|
||||
'discard': __('Encrypt email (if not possible, discard notification)')
|
||||
'always': __('Encrypt email (if not possible, send notification anyway)')
|
||||
}
|
||||
value: meta.encryption
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-encryption').html(selection)
|
||||
|
||||
@buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0)
|
||||
|
||||
elementRow.find('.js-setArticle').empty()
|
||||
|
||||
name = "#{attribute.name}::article.#{articleType}"
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
label: __('Visibility')
|
||||
options: { true: 'internal', false: 'public' }
|
||||
value: meta.internal
|
||||
translate: true
|
||||
)
|
||||
articleElement = $( App.view('generic/ticket_perform_action/article')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
articleType: articleType
|
||||
meta: meta || {}
|
||||
))
|
||||
articleElement.find('.js-internal').html(selection)
|
||||
articleElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: __('message')
|
||||
maxlength: 200000
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: articleElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: __('Ticket')
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: __('Article')
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: __('Current User')
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setArticle').html(articleElement).removeClass('hide')
|
||||
@@ -106,6 +106,8 @@ class App.UiElement.ApplicationSelector
|
||||
config.expanding = false
|
||||
if config.type is 'email' || config.type is 'tel'
|
||||
config.type = 'text'
|
||||
if config.tag is 'select'
|
||||
config.multiple = true
|
||||
for operatorRegEx, operator of operators_type
|
||||
myRegExp = new RegExp(operatorRegEx, 'i')
|
||||
if config.tag && config.tag.match(myRegExp)
|
||||
@@ -505,11 +507,7 @@ class App.UiElement.ApplicationSelector
|
||||
config.guess = false
|
||||
if config.tag is 'checkbox'
|
||||
config.tag = 'select'
|
||||
tagSearch = "#{config.tag}_search"
|
||||
if App.UiElement[tagSearch]
|
||||
item = App.UiElement[tagSearch].render(config, {})
|
||||
else
|
||||
item = App.UiElement[config.tag].render(config, {})
|
||||
item = @renderConfig(config, meta)
|
||||
if meta.operator is 'before (relative)' || meta.operator is 'within next (relative)' || meta.operator is 'within last (relative)' || meta.operator is 'after (relative)' || meta.operator is 'from (relative)' || meta.operator is 'till (relative)'
|
||||
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
@@ -523,6 +521,11 @@ class App.UiElement.ApplicationSelector
|
||||
else
|
||||
elementRow.find('.js-value').removeClass('hide')
|
||||
|
||||
@renderConfig: (config, meta) ->
|
||||
tagSearch = "#{config.tag}_search"
|
||||
return App.UiElement[tagSearch].render(config, {}) if App.UiElement[tagSearch]
|
||||
return App.UiElement[config.tag].render(config, {})
|
||||
|
||||
@buildValueConfigMultiple: (config, meta) ->
|
||||
config.multiple = true
|
||||
config.nulloption = false
|
||||
|
||||
@@ -161,6 +161,8 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
|
||||
config.expanding = false
|
||||
if config.type is 'email' || config.type is 'tel'
|
||||
config.type = 'text'
|
||||
if config.tag.match(/^(tree_)?select$/)
|
||||
config.multiple = true
|
||||
for operatorRegEx, operator of operatorsType
|
||||
myRegExp = new RegExp(operatorRegEx, 'i')
|
||||
if config.tag && config.tag.match(myRegExp)
|
||||
|
||||
@@ -75,6 +75,8 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
||||
config.default = undefined
|
||||
if config.tag is 'boolean'
|
||||
config.tag = 'select'
|
||||
if config.tag.match(/^(tree_)?select$/)
|
||||
config.multiple = true
|
||||
if config.type is 'email' || config.type is 'tel'
|
||||
config.type = 'text'
|
||||
for operatorRegEx, operator of operatorsType
|
||||
@@ -129,12 +131,22 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
||||
@buildValueConfigMultiple: (config, meta) ->
|
||||
if _.contains(['add_option', 'remove_option', 'set_fixed_to', 'select'], meta.operator)
|
||||
config.multiple = true
|
||||
if config.data_type.match(/^(tree_)?select$/) && meta.operator is 'select'
|
||||
config.multiple = false
|
||||
|
||||
config.nulloption = true
|
||||
else
|
||||
config.multiple = false
|
||||
config.nulloption = false
|
||||
return config
|
||||
|
||||
@renderConfig: (config, meta) ->
|
||||
if _.contains(['add_option', 'remove_option', 'set_fixed_to'], meta.operator)
|
||||
tagSearch = "#{config.tag}_search"
|
||||
return App.UiElement[tagSearch].render(config, {}) if App.UiElement[tagSearch]
|
||||
|
||||
return App.UiElement[config.tag].render(config, {})
|
||||
|
||||
@mapOperatorDisplayName: (operator) ->
|
||||
names =
|
||||
'show': __('show')
|
||||
|
||||
@@ -25,11 +25,29 @@ class App.UiElement.permission extends App.UiElement.ApplicationUiElement
|
||||
groupAccesses: App.Group.accesses()
|
||||
) )
|
||||
|
||||
item.find('div.js-groupList input[type="checkbox"]').on('change', (e) ->
|
||||
checked = item.find('div.js-groupList input[type="checkbox"]:checked').length > 0 ? true : false
|
||||
parentCheckbox = item.find('div.js-groupList input[type="checkbox"]').parents('.js-subPermissionList').find('input[type="checkbox"][data-permission-name]')
|
||||
|
||||
parentCheckbox.prop('checked', checked)
|
||||
)
|
||||
|
||||
item.find('div.js-groupList input[type="checkbox"]').parents('.js-subPermissionList').find('input[type="checkbox"][data-permission-name]').on('change', (e) ->
|
||||
return if $(e.currentTarget).prop('checked')
|
||||
item.find('div.js-groupList input[type="checkbox"]:checked').prop('checked', false)
|
||||
)
|
||||
|
||||
# show/hide trees
|
||||
item.find('[name=permission_ids]').on('change', (e) =>
|
||||
@checkUncheck($(e.currentTarget), permissions, item)
|
||||
)
|
||||
item.find('[name=permission_ids]').trigger('change')
|
||||
|
||||
# check/uncheck group permissions (particular permissions vs. full)
|
||||
item.on('click', '.checkbox-replacement', (event) ->
|
||||
App.PermissionHelper.switchGroupPermission(event)
|
||||
)
|
||||
|
||||
item
|
||||
|
||||
@checkUncheck: (element, permissions, item) ->
|
||||
@@ -54,4 +72,3 @@ class App.UiElement.permission extends App.UiElement.ApplicationUiElement
|
||||
lookupPermission = App.Permission.findByAttribute('name', localPermission)
|
||||
if lookupPermission
|
||||
item.find("[name=permission_ids][value=#{lookupPermission.id}]").prop('checked', false)
|
||||
|
||||
|
||||
@@ -1,78 +1,20 @@
|
||||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.postmaster_set
|
||||
@defaults: ->
|
||||
class App.UiElement.postmaster_set extends App.UiElement.ApplicationAction
|
||||
@defaults: (attribute) ->
|
||||
defaults = ['x-zammad-ticket-state_id']
|
||||
|
||||
groups =
|
||||
ticket:
|
||||
name: 'Ticket'
|
||||
name: __('Ticket')
|
||||
model: 'Ticket'
|
||||
options: [
|
||||
{
|
||||
value: 'priority_id'
|
||||
name: __('Priority')
|
||||
relation: 'TicketPriority'
|
||||
}
|
||||
{
|
||||
value: 'state_id'
|
||||
name: __('State')
|
||||
relation: 'TicketState'
|
||||
}
|
||||
{
|
||||
value: 'tags'
|
||||
name: __('Tag')
|
||||
tag: 'tag'
|
||||
}
|
||||
{
|
||||
value: 'customer_id'
|
||||
name: __('Customer')
|
||||
relation: 'User'
|
||||
tag: 'user_autocompletion'
|
||||
disableCreateObject: true
|
||||
}
|
||||
{
|
||||
value: 'group_id'
|
||||
name: __('Group')
|
||||
relation: 'Group'
|
||||
}
|
||||
{
|
||||
value: 'owner_id'
|
||||
name: __('Owner')
|
||||
relation: 'User'
|
||||
tag: 'user_autocompletion'
|
||||
disableCreateObject: true
|
||||
}
|
||||
]
|
||||
article:
|
||||
name: 'Article'
|
||||
options: [
|
||||
{
|
||||
value: 'x-zammad-article-internal'
|
||||
name: __('Internal')
|
||||
options: { true: 'yes', false: 'no'}
|
||||
}
|
||||
{
|
||||
value: 'x-zammad-article-type_id'
|
||||
name: __('Type')
|
||||
relation: 'TicketArticleType'
|
||||
}
|
||||
{
|
||||
value: 'x-zammad-article-sender_id'
|
||||
name: __('Sender')
|
||||
relation: 'TicketArticleSender'
|
||||
}
|
||||
]
|
||||
expert:
|
||||
name: 'Expert'
|
||||
options: [
|
||||
{
|
||||
value: 'x-zammad-ignore'
|
||||
name: __('Ignore Message')
|
||||
options: { true: 'yes', false: 'no'}
|
||||
}
|
||||
]
|
||||
|
||||
elements = {}
|
||||
for groupKey, groupMeta of groups
|
||||
if groupMeta.model && App[groupMeta.model]
|
||||
if groupMeta.model
|
||||
for row in App[groupMeta.model].configure_attributes
|
||||
|
||||
# ignore passwords and relations
|
||||
@@ -81,195 +23,24 @@ class App.UiElement.postmaster_set
|
||||
# ignore readonly attributes
|
||||
if !row.readonly
|
||||
config = _.clone(row)
|
||||
if config.tag is 'tag'
|
||||
config.operator = ['add', 'remove']
|
||||
elements["x-zammad-ticket-#{config.name}"] = config
|
||||
|
||||
# add additional ticket attributes
|
||||
for row in App.Ticket.configure_attributes
|
||||
exists = false
|
||||
for item in groups.ticket.options
|
||||
if item.value is row.name
|
||||
exists = true
|
||||
switch config.tag
|
||||
when 'datetime'
|
||||
config.operator = ['static', 'relative']
|
||||
when 'tag'
|
||||
config.operator = ['add', 'remove']
|
||||
|
||||
# do not support this types
|
||||
else if row.tag is 'datetime' || row.tag is 'date' || row.tag is 'tag'
|
||||
exists = true
|
||||
elements["x-zammad-#{groupKey}-#{config.name}"] = config
|
||||
|
||||
# ignore passwords and relations
|
||||
if !exists && row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
|
||||
elements['x-zammad-article-internal'] = _.clone(App.TicketArticle.attributesGet()['internal'])
|
||||
elements['x-zammad-article-internal'].null = false
|
||||
|
||||
# ignore readonly attributes
|
||||
if !row.readonly
|
||||
item =
|
||||
value: row.name
|
||||
name: row.display
|
||||
relation: row.relation
|
||||
tag: row.tag
|
||||
options: row.options
|
||||
groups.ticket.options.push item
|
||||
elements['x-zammad-article-type_id'] = _.clone(App.TicketArticle.attributesGet()['type_id'])
|
||||
elements['x-zammad-article-sender_id'] = _.clone(App.TicketArticle.attributesGet()['sender_id'])
|
||||
elements['x-zammad-ignore'] = { name: 'x-zammad-ignore', display: __('Ignore Message'), tag: 'boolean', type: 'boolean', null: false }
|
||||
|
||||
for item in groups.ticket.options
|
||||
item.value = "x-zammad-ticket-#{item.value}"
|
||||
[defaults, groups, elements]
|
||||
|
||||
[elements, groups]
|
||||
|
||||
@placeholder: (elementFull, attribute, params = {}, groups) ->
|
||||
item = $( App.view('generic/postmaster_set_row')(attribute: attribute) )
|
||||
selector = @buildAttributeSelector(elementFull, groups, attribute, item)
|
||||
item.find('.js-attributeSelector').prepend(selector)
|
||||
item
|
||||
|
||||
@render: (attribute, params = {}) ->
|
||||
|
||||
[elements, groups] = @defaults()
|
||||
|
||||
# scaffold of match elements
|
||||
item = $( App.view('generic/postmaster_set')(attribute: attribute) )
|
||||
|
||||
# add filter
|
||||
item.on('click', '.js-add', (e) =>
|
||||
element = $(e.target).closest('.js-filterElement')
|
||||
placeholder = @placeholder(item, attribute, params, groups)
|
||||
if element.get(0)
|
||||
element.after(placeholder)
|
||||
else
|
||||
item.append(placeholder)
|
||||
placeholder.find('.js-attributeSelector select').trigger('change')
|
||||
)
|
||||
|
||||
# remove filter
|
||||
item.on('click', '.js-remove', (e) =>
|
||||
return if $(e.currentTarget).hasClass('is-disabled')
|
||||
$(e.target).closest('.js-filterElement').remove()
|
||||
@rebuildAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change attribute selector
|
||||
item.on('change', '.js-attributeSelector select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@rebuildAttributeSelectors(item, elementRow, groupAndAttribute, attribute)
|
||||
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
@buildValue(item, elementRow, groupAndAttribute, groups, undefined, undefined, attribute)
|
||||
)
|
||||
|
||||
# build initial params
|
||||
if _.isEmpty(params[attribute.name])
|
||||
element = @placeholder(item, attribute, params, groups)
|
||||
item.append(element)
|
||||
groupAndAttribute = element.find('.js-attributeSelector option:selected').attr('value')
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, attribute)
|
||||
@buildOperator(item, element, groupAndAttribute, elements, {}, attribute)
|
||||
@buildValue(item, element, groupAndAttribute, groups, undefined, undefined, attribute)
|
||||
return item
|
||||
|
||||
else
|
||||
for key, meta of params[attribute.name]
|
||||
operator = meta.operator
|
||||
value = meta.value
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups)
|
||||
groupAndAttribute = element.find('.js-attributeSelector option:selected').attr('value')
|
||||
@rebuildAttributeSelectors(item, element, key, attribute)
|
||||
@buildOperator(item, element, key, elements, {}, attribute)
|
||||
@buildValue(item, element, key, groups, value, operator, attribute)
|
||||
|
||||
item.append(element)
|
||||
item.find('.js-attributeSelector select').trigger('change')
|
||||
|
||||
item
|
||||
|
||||
@buildValue: (elementFull, elementRow, key, groups, value, operator, attribute) ->
|
||||
|
||||
# do nothing if item already exists
|
||||
name = "#{attribute.name}::#{key}::value"
|
||||
return if elementRow.find("[name=\"#{name}\"]").get(0)
|
||||
config = {}
|
||||
for groupName, meta of groups
|
||||
for entry in meta.options
|
||||
if entry.value is key
|
||||
config = clone(entry)
|
||||
if !config.tag
|
||||
if config.relation || config.options
|
||||
config['tag'] = 'select'
|
||||
else
|
||||
config['tag'] = 'input'
|
||||
config['type'] = 'text'
|
||||
config['name'] = name
|
||||
if !value && attribute.value && attribute.value[key]
|
||||
config['value'] = attribute.value[key].value
|
||||
else
|
||||
config['value'] = value
|
||||
item = App.UiElement[config.tag].render(config, {})
|
||||
elementRow.find('.js-value').html(item)
|
||||
|
||||
@buildAttributeSelector: (elementFull, groups, attribute) ->
|
||||
|
||||
# find first possible attribute
|
||||
selectedValue = ''
|
||||
elementFull.find('.js-attributeSelector select option').each(->
|
||||
if !selectedValue && !$(@).prop('disabled')
|
||||
selectedValue = $(@).val()
|
||||
)
|
||||
|
||||
selection = $('<select class="form-control"></select>')
|
||||
for groupKey, groupMeta of groups
|
||||
displayName = App.i18n.translateInline(groupMeta.name)
|
||||
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
|
||||
optgroup = selection.find("optgroup.js-#{groupKey}")
|
||||
for entry in groupMeta.options
|
||||
displayName = App.i18n.translateInline(entry.name)
|
||||
selected = ''
|
||||
if entry.value is selectedValue
|
||||
selected = 'selected="selected"'
|
||||
optgroup.append("<option value=\"#{entry.value}\" #{selected}>#{displayName}</option>")
|
||||
selection
|
||||
|
||||
@rebuildAttributeSelectors: (elementFull, elementRow, key, attribute) ->
|
||||
|
||||
# enable all
|
||||
elementFull.find('.js-attributeSelector select option').prop('disabled', false)
|
||||
|
||||
# disable all used attributes
|
||||
elementFull.find('.js-attributeSelector select').each(->
|
||||
keyLocal = $(@).val()
|
||||
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
|
||||
)
|
||||
|
||||
# disable - if we only have one attribute
|
||||
if elementFull.find('.js-attributeSelector select').length > 1
|
||||
elementFull.find('.js-remove').removeClass('is-disabled')
|
||||
else
|
||||
elementFull.find('.js-remove').addClass('is-disabled')
|
||||
|
||||
# set attribute
|
||||
if key
|
||||
elementRow.find('.js-attributeSelector select').val(key)
|
||||
|
||||
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
|
||||
if !meta.operator
|
||||
meta.operator = currentOperator
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::operator"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
|
||||
if !attributeConfig || !attributeConfig.operator
|
||||
elementRow.find('.js-operator').parent().addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-operator').parent().removeClass('hide')
|
||||
if attributeConfig && attributeConfig.operator
|
||||
for operator in attributeConfig.operator
|
||||
operatorName = App.i18n.translateInline(operator)
|
||||
selected = ''
|
||||
if meta.operator is operator
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
|
||||
selection
|
||||
|
||||
elementRow.find('.js-operator select').replaceWith(selection)
|
||||
@elementKeyGroup: (elementKey) ->
|
||||
return 'expert' if elementKey is 'x-zammad-ignore'
|
||||
elementKey.replace('x-zammad-', '').split(/-/)[0]
|
||||
|
||||
@@ -4,7 +4,10 @@ class App.UiElement.select_search extends App.UiElement.ApplicationUiElement
|
||||
attribute = $.extend(true, {}, attributeConfig)
|
||||
|
||||
# set multiple option
|
||||
attribute.multiple = 'multiple'
|
||||
if attribute.multiple
|
||||
attribute.multiple = 'multiple'
|
||||
else
|
||||
attribute.multiple = ''
|
||||
|
||||
delete attribute.filter
|
||||
|
||||
|
||||
@@ -1,575 +1,2 @@
|
||||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.ticket_perform_action
|
||||
@defaults: (attribute) ->
|
||||
defaults = ['ticket.state_id']
|
||||
|
||||
groups =
|
||||
ticket:
|
||||
name: __('Ticket')
|
||||
model: 'Ticket'
|
||||
article:
|
||||
name: __('Article')
|
||||
model: 'Article'
|
||||
|
||||
if attribute.notification
|
||||
groups.notification =
|
||||
name: __('Notification')
|
||||
model: 'Notification'
|
||||
|
||||
# merge config
|
||||
elements = {}
|
||||
for groupKey, groupMeta of groups
|
||||
if !groupMeta.model || !App[groupMeta.model]
|
||||
if groupKey is 'notification'
|
||||
elements["#{groupKey}.email"] = { name: 'email', display: __('Email') }
|
||||
elements["#{groupKey}.sms"] = { name: 'sms', display: __('SMS') }
|
||||
elements["#{groupKey}.webhook"] = { name: 'webhook', display: __('Webhook') }
|
||||
else if groupKey is 'article'
|
||||
elements["#{groupKey}.note"] = { name: 'note', display: __('Note') }
|
||||
else
|
||||
|
||||
for row in App[groupMeta.model].configure_attributes
|
||||
|
||||
# ignore passwords and relations
|
||||
if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
|
||||
|
||||
# ignore readonly attributes
|
||||
if !row.readonly
|
||||
config = _.clone(row)
|
||||
|
||||
switch config.tag
|
||||
when 'datetime'
|
||||
config.operator = ['static', 'relative']
|
||||
when 'tag'
|
||||
config.operator = ['add', 'remove']
|
||||
|
||||
elements["#{groupKey}.#{config.name}"] = config
|
||||
|
||||
# add ticket deletion action
|
||||
if attribute.ticket_delete
|
||||
elements['ticket.action'] =
|
||||
name: 'action'
|
||||
display: __('Action')
|
||||
tag: 'select'
|
||||
null: false
|
||||
translate: true
|
||||
options:
|
||||
delete: 'Delete'
|
||||
|
||||
[defaults, groups, elements]
|
||||
|
||||
@placeholder: (elementFull, attribute, params, groups, elements) ->
|
||||
item = $( App.view('generic/ticket_perform_action/row')( attribute: attribute ) )
|
||||
selector = @buildAttributeSelector(elementFull, groups, elements)
|
||||
item.find('.js-attributeSelector').prepend(selector)
|
||||
item
|
||||
|
||||
@render: (attribute, params = {}) ->
|
||||
|
||||
[defaults, groups, elements] = @defaults(attribute)
|
||||
|
||||
# return item
|
||||
item = $( App.view('generic/ticket_perform_action/index')( attribute: attribute ) )
|
||||
|
||||
# add filter
|
||||
item.on('click', '.js-rowActions .js-add', (e) =>
|
||||
element = $(e.target).closest('.js-filterElement')
|
||||
placeholder = @placeholder(item, attribute, params, groups, elements)
|
||||
if element.get(0)
|
||||
element.after(placeholder)
|
||||
else
|
||||
item.append(placeholder)
|
||||
placeholder.find('.js-attributeSelector select').trigger('change')
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# remove filter
|
||||
item.on('click', '.js-rowActions .js-remove', (e) =>
|
||||
return if $(e.currentTarget).hasClass('is-disabled')
|
||||
$(e.target).closest('.js-filterElement').remove()
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change attribute selector
|
||||
item.on('change', '.js-attributeSelector select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
@updateAttributeSelectors(item)
|
||||
)
|
||||
|
||||
# change operator selector
|
||||
item.on('change', '.js-operator select', (e) =>
|
||||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
)
|
||||
|
||||
# build initial params
|
||||
if _.isEmpty(params[attribute.name])
|
||||
|
||||
for groupAndAttribute in defaults
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
item.append(element)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, {}, attribute)
|
||||
|
||||
else
|
||||
|
||||
for groupAndAttribute, meta of params[attribute.name]
|
||||
|
||||
# build and append
|
||||
element = @placeholder(item, attribute, params, groups, elements)
|
||||
@rebuildAttributeSelectors(item, element, groupAndAttribute, elements, meta, attribute)
|
||||
item.append(element)
|
||||
|
||||
@disableRemoveForOneAttribute(item)
|
||||
item
|
||||
|
||||
@buildAttributeSelector: (elementFull, groups, elements) ->
|
||||
|
||||
# find first possible attribute
|
||||
selectedValue = ''
|
||||
elementFull.find('.js-attributeSelector select option').each(->
|
||||
if !selectedValue && !$(@).prop('disabled')
|
||||
selectedValue = $(@).val()
|
||||
)
|
||||
|
||||
selection = $('<select class="form-control"></select>')
|
||||
for groupKey, groupMeta of groups
|
||||
displayName = App.i18n.translateInline(groupMeta.name)
|
||||
selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
|
||||
optgroup = selection.find("optgroup.js-#{groupKey}")
|
||||
for elementKey, elementGroup of elements
|
||||
spacer = elementKey.split(/\./)
|
||||
if spacer[0] is groupKey
|
||||
attributeConfig = elements[elementKey]
|
||||
displayName = App.i18n.translateInline(attributeConfig.display)
|
||||
|
||||
selected = ''
|
||||
if elementKey is selectedValue
|
||||
selected = 'selected="selected"'
|
||||
optgroup.append("<option value=\"#{elementKey}\" #{selected}>#{displayName}</option>")
|
||||
selection
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute: (elementFull) ->
|
||||
if elementFull.find('.js-attributeSelector select').length > 1
|
||||
elementFull.find('.js-remove').removeClass('is-disabled')
|
||||
else
|
||||
elementFull.find('.js-remove').addClass('is-disabled')
|
||||
|
||||
@updateAttributeSelectors: (elementFull) ->
|
||||
|
||||
# enable all
|
||||
elementFull.find('.js-attributeSelector select option').prop('disabled', false)
|
||||
|
||||
# disable all used attributes
|
||||
elementFull.find('.js-attributeSelector select').each(->
|
||||
keyLocal = $(@).val()
|
||||
elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
|
||||
)
|
||||
|
||||
# disable - if we only have one attribute
|
||||
@disableRemoveForOneAttribute(elementFull)
|
||||
|
||||
@rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
# set attribute
|
||||
if groupAndAttribute
|
||||
elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
|
||||
|
||||
notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
|
||||
articleTypeMatch = groupAndAttribute.match(/^article.([\w]+)$/)
|
||||
|
||||
if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
@buildNotificationArea(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else if _.isArray(articleTypeMatch) && articleType = articleTypeMatch[1]
|
||||
elementRow.find('.js-setAttribute').html('').addClass('hide')
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
@buildArticleArea(articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
else
|
||||
elementRow.find('.js-setNotification').html('').addClass('hide')
|
||||
elementRow.find('.js-setArticle').html('').addClass('hide')
|
||||
if !elementRow.find('.js-setAttribute div').get(0)
|
||||
attributeSelectorElement = $( App.view('generic/ticket_perform_action/attribute_selector')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
meta: meta || {}
|
||||
))
|
||||
elementRow.find('.js-setAttribute').html(attributeSelectorElement).removeClass('hide')
|
||||
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
|
||||
if !meta.operator
|
||||
meta.operator = currentOperator
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::operator"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
if !attributeConfig || !attributeConfig.operator
|
||||
elementRow.find('.js-operator').parent().addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-operator').parent().removeClass('hide')
|
||||
if attributeConfig && attributeConfig.operator
|
||||
for operator in attributeConfig.operator
|
||||
operatorName = App.i18n.translateInline(operator)
|
||||
selected = ''
|
||||
if meta.operator is operator
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
|
||||
selection
|
||||
|
||||
elementRow.find('.js-operator select').replaceWith(selection)
|
||||
|
||||
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
|
||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
|
||||
if !meta.pre_condition
|
||||
meta.pre_condition = currentPreCondition
|
||||
|
||||
toggleValue = =>
|
||||
preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
if preCondition isnt 'specific'
|
||||
elementRow.find('.js-value select').html('')
|
||||
elementRow.find('.js-value').addClass('hide')
|
||||
else
|
||||
elementRow.find('.js-value').removeClass('hide')
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
|
||||
# force to use auto complition on user lookup
|
||||
attribute = clone(attributeConfig, true)
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
attributeSelected = elements[groupAndAttribute]
|
||||
|
||||
preCondition = false
|
||||
if attributeSelected.relation is 'User'
|
||||
preCondition = 'user'
|
||||
attribute.tag = 'user_autocompletion'
|
||||
if attributeSelected.relation is 'Organization'
|
||||
preCondition = 'org'
|
||||
attribute.tag = 'autocompletion_ajax'
|
||||
if !preCondition
|
||||
elementRow.find('.js-preCondition select').html('')
|
||||
elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
|
||||
toggleValue()
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
return
|
||||
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
|
||||
|
||||
selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
|
||||
options = {}
|
||||
if preCondition is 'user'
|
||||
options =
|
||||
'current_user.id': App.i18n.translateInline('current user')
|
||||
'specific': App.i18n.translateInline('specific user')
|
||||
|
||||
if attributeSelected.null is true
|
||||
options['not_set'] = App.i18n.translateInline('unassign user')
|
||||
|
||||
else if preCondition is 'org'
|
||||
options =
|
||||
'current_user.organization_id': App.i18n.translateInline('current user organization')
|
||||
'specific': App.i18n.translateInline('specific organization')
|
||||
|
||||
for key, value of options
|
||||
selected = ''
|
||||
if key is meta.pre_condition
|
||||
selected = 'selected="selected"'
|
||||
selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
|
||||
elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
|
||||
elementRow.find('.js-preCondition select').replaceWith(selection)
|
||||
|
||||
elementRow.find('.js-preCondition select').on('change', (e) ->
|
||||
toggleValue()
|
||||
)
|
||||
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
toggleValue()
|
||||
|
||||
@buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::value"
|
||||
|
||||
# build new item
|
||||
attributeConfig = elements[groupAndAttribute]
|
||||
config = clone(attributeConfig, true)
|
||||
|
||||
if config.relation is 'User'
|
||||
config.tag = 'user_autocompletion'
|
||||
if config.relation is 'Organization'
|
||||
config.tag = 'autocompletion_ajax'
|
||||
|
||||
# render ui element
|
||||
item = ''
|
||||
if config && App.UiElement[config.tag]
|
||||
config['name'] = name
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
|
||||
config.multiple = false
|
||||
config.default = undefined
|
||||
config.nulloption = config.null
|
||||
if config.tag is 'multiselect' || config.tag is 'multi_tree_select'
|
||||
config.multiple = true
|
||||
if config.tag is 'checkbox'
|
||||
config.tag = 'select'
|
||||
if config.tag is 'datetime'
|
||||
config.validationContainer = 'self'
|
||||
item = App.UiElement[config.tag].render(config, {})
|
||||
|
||||
relative_operators = [
|
||||
'before (relative)',
|
||||
'within next (relative)',
|
||||
'within last (relative)',
|
||||
'after (relative)',
|
||||
'till (relative)',
|
||||
'from (relative)',
|
||||
'relative'
|
||||
]
|
||||
|
||||
upcoming_operator = meta.operator
|
||||
|
||||
if !_.include(config.operator, upcoming_operator)
|
||||
if Array.isArray(config.operator)
|
||||
upcoming_operator = config.operator[0]
|
||||
else
|
||||
upcoming_operator = null
|
||||
|
||||
if _.include(relative_operators, upcoming_operator)
|
||||
config['name'] = "#{attribute.name}::#{groupAndAttribute}"
|
||||
if attribute.value && attribute.value[groupAndAttribute]
|
||||
config['value'] = _.clone(attribute.value[groupAndAttribute])
|
||||
item = App.UiElement['time_range'].render(config, {})
|
||||
|
||||
elementRow.find('.js-setAttribute > .flex > .js-value').removeClass('hide').html(item)
|
||||
|
||||
@buildNotificationArea: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
|
||||
|
||||
elementRow.find('.js-setNotification').empty()
|
||||
|
||||
options =
|
||||
'article_last_sender': __('Sender of last article')
|
||||
'ticket_owner': __('Owner')
|
||||
'ticket_customer': __('Customer')
|
||||
'ticket_agents': __('All agents')
|
||||
|
||||
name = "#{attribute.name}::notification.#{notificationType}"
|
||||
|
||||
messageLength = switch notificationType
|
||||
when 'sms' then 160
|
||||
else 200000
|
||||
|
||||
# meta.recipient was a string in the past (single-select) so we convert it to array if needed
|
||||
if !_.isArray(meta.recipient)
|
||||
meta.recipient = [meta.recipient]
|
||||
|
||||
columnSelectOptions = []
|
||||
for key, value of options
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectOptions.push({ value: key, name: App.i18n.translatePlain(value), selected: selected })
|
||||
|
||||
columnSelectRecipientUserOptions = []
|
||||
for user in App.User.all()
|
||||
key = "userid_#{user.id}"
|
||||
selected = undefined
|
||||
for recipient in meta.recipient
|
||||
if key is recipient
|
||||
selected = true
|
||||
columnSelectRecipientUserOptions.push({ value: key, name: "#{user.firstname} #{user.lastname}", selected: selected })
|
||||
|
||||
columnSelectRecipient = new App.ColumnSelect
|
||||
attribute:
|
||||
name: "#{name}::recipient"
|
||||
options: [
|
||||
{
|
||||
label: __('Variables'),
|
||||
group: columnSelectOptions
|
||||
},
|
||||
{
|
||||
label: __('User'),
|
||||
group: columnSelectRecipientUserOptions
|
||||
},
|
||||
]
|
||||
|
||||
selectionRecipient = columnSelectRecipient.element()
|
||||
|
||||
if notificationType is 'webhook'
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/webhook')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
|
||||
if App.Webhook.search(filter: { active: true }).length isnt 0 || !_.isEmpty(meta.webhook_id)
|
||||
webhookSelection = App.UiElement.select.render(
|
||||
name: "#{name}::webhook_id"
|
||||
multiple: false
|
||||
null: false
|
||||
relation: 'Webhook'
|
||||
value: meta.webhook_id
|
||||
translate: false
|
||||
nulloption: true
|
||||
)
|
||||
else
|
||||
webhookSelection = App.view('generic/ticket_perform_action/webhook_not_available')( attribute: attribute )
|
||||
|
||||
notificationElement.find('.js-webhooks').html(webhookSelection)
|
||||
|
||||
else
|
||||
notificationElement = $( App.view('generic/ticket_perform_action/notification')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
notificationType: notificationType
|
||||
meta: meta || {}
|
||||
))
|
||||
|
||||
notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
|
||||
|
||||
visibilitySelection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: __('internal'), false: __('public') }
|
||||
value: meta.internal || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
includeAttachmentsCheckbox = App.UiElement.select.render(
|
||||
name: "#{name}::include_attachments"
|
||||
multiple: false
|
||||
null: false
|
||||
options: { true: __('Yes'), false: __('No') }
|
||||
value: meta.include_attachments || 'false'
|
||||
translate: true
|
||||
)
|
||||
|
||||
notificationElement.find('.js-internal').html(visibilitySelection)
|
||||
notificationElement.find('.js-include_attachments').html(includeAttachmentsCheckbox)
|
||||
|
||||
notificationElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: __('message')
|
||||
maxlength: messageLength
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: __('Ticket')
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: __('Article')
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: __('Current User')
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide')
|
||||
|
||||
if App.Config.get('smime_integration') == true
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::sign"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': __('Do not sign email')
|
||||
'discard': __('Sign email (if not possible, discard notification)')
|
||||
'always': __('Sign email (if not possible, send notification anyway)')
|
||||
}
|
||||
value: meta.sign
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-sign').html(selection)
|
||||
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::encryption"
|
||||
multiple: false
|
||||
options: {
|
||||
'no': __('Do not encrypt email')
|
||||
'discard': __('Encrypt email (if not possible, discard notification)')
|
||||
'always': __('Encrypt email (if not possible, send notification anyway)')
|
||||
}
|
||||
value: meta.encryption
|
||||
translate: true
|
||||
)
|
||||
|
||||
elementRow.find('.js-encryption').html(selection)
|
||||
|
||||
@buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
|
||||
|
||||
return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0)
|
||||
|
||||
elementRow.find('.js-setArticle').empty()
|
||||
|
||||
name = "#{attribute.name}::article.#{articleType}"
|
||||
selection = App.UiElement.select.render(
|
||||
name: "#{name}::internal"
|
||||
multiple: false
|
||||
null: false
|
||||
label: __('Visibility')
|
||||
options: { true: 'internal', false: 'public' }
|
||||
value: meta.internal
|
||||
translate: true
|
||||
)
|
||||
articleElement = $( App.view('generic/ticket_perform_action/article')(
|
||||
attribute: attribute
|
||||
name: name
|
||||
articleType: articleType
|
||||
meta: meta || {}
|
||||
))
|
||||
articleElement.find('.js-internal').html(selection)
|
||||
articleElement.find('.js-body div[contenteditable="true"]').ce(
|
||||
mode: 'richtext'
|
||||
placeholder: __('message')
|
||||
maxlength: 200000
|
||||
)
|
||||
new App.WidgetPlaceholder(
|
||||
el: articleElement.find('.js-body div[contenteditable="true"]').parent()
|
||||
objects: [
|
||||
{
|
||||
prefix: 'ticket'
|
||||
object: 'Ticket'
|
||||
display: __('Ticket')
|
||||
},
|
||||
{
|
||||
prefix: 'article'
|
||||
object: 'TicketArticle'
|
||||
display: __('Article')
|
||||
},
|
||||
{
|
||||
prefix: 'user'
|
||||
object: 'User'
|
||||
display: __('Current User')
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
elementRow.find('.js-setArticle').html(articleElement).removeClass('hide')
|
||||
class App.UiElement.ticket_perform_action extends App.UiElement.ApplicationAction
|
||||
|
||||
@@ -27,14 +27,21 @@ class App.UiElement.time_range
|
||||
@localRenderPulldown(element.filter('.js-valueRangeSelector'), values[range], attribute)
|
||||
element.find('select.form-control.js-range').on('change', (e) =>
|
||||
range = $(e.currentTarget).val()
|
||||
@localRenderPulldown($(e.currentTarget).closest('.js-filterElement').find('.js-valueRangeSelector'), values[range], attribute)
|
||||
selected_value_name = $(e.currentTarget).prop('name').replace(/::\w+$/, '::value')
|
||||
selected_value = $("select[name='#{selected_value_name}']").val() if selected_value_name
|
||||
@localRenderPulldown($(e.currentTarget).closest('.js-filterElement').find('.js-valueRangeSelector'), values[range], attribute, selected_value)
|
||||
)
|
||||
element
|
||||
|
||||
@localRenderPulldown: (el, range, attribute) ->
|
||||
@localRenderPulldown: (el, range, attribute, selected_value) ->
|
||||
return if !range
|
||||
values = {}
|
||||
for count in range
|
||||
values[count.toString()] = count.toString()
|
||||
select = App.view('generic/time_range_value_selector')(attribute: attribute, values: values)
|
||||
if !selected_value
|
||||
if attribute.value
|
||||
selected_value = attribute.value.value
|
||||
else
|
||||
selected_value = 1
|
||||
select = App.view('generic/time_range_value_selector')(attribute: attribute, values: values, selected_value: selected_value)
|
||||
el.html(select)
|
||||
|
||||
@@ -74,18 +74,10 @@ class App.UiElement.user_permission
|
||||
groupAccesses: App.Group.accesses()
|
||||
) )
|
||||
|
||||
throttled = _.throttle( (e) ->
|
||||
input = $(@).find('input')
|
||||
upcoming_state = if $(e.target).is(':checkbox') then input.prop('checked') else !input.prop('checked')
|
||||
value = input.val()
|
||||
|
||||
if value is 'full' and upcoming_state is true
|
||||
$(@).closest('tr').find('input:not([value=full])').prop('checked', false)
|
||||
else if value isnt 'full' and upcoming_state is true
|
||||
$(@).closest('tr').find('input[value=full]').prop('checked', false)
|
||||
, 300, { trailing: false })
|
||||
|
||||
item.on('click', '.checkbox-replacement', throttled)
|
||||
# check/uncheck group permissions (particular permissions vs. full)
|
||||
item.on('click', '.checkbox-replacement', (event) ->
|
||||
App.PermissionHelper.switchGroupPermission(event)
|
||||
)
|
||||
|
||||
# if customer, remove admin and agent
|
||||
item.find('[name=role_ids]').on('change', (e) =>
|
||||
|
||||
@@ -181,7 +181,7 @@ class App.TicketCreate extends App.Controller
|
||||
show: =>
|
||||
@navupdate("#ticket/create/id/#{@id}#{@split}", type: 'menu')
|
||||
@autosaveStart()
|
||||
@controllerBind('ticket_create_rerender', (template) => @renderQueue(template))
|
||||
@controllerBind('ticket_create_rerender', (template) => @renderQueue(template))
|
||||
@controllerBind('ticket_create_shared_draft_saved', @sharedDraftSaved)
|
||||
@controllerBind('ticket_create_import_draft_attachments', @importDraftAttachments)
|
||||
|
||||
@@ -192,7 +192,7 @@ class App.TicketCreate extends App.Controller
|
||||
|
||||
hide: =>
|
||||
@autosaveStop()
|
||||
@controllerUnbind('ticket_create_rerender', (template) => @renderQueue(template))
|
||||
@controllerUnbind('ticket_create_rerender')
|
||||
@controllerUnbind('ticket_create_shared_draft_saved')
|
||||
@controllerUnbind('ticket_create_import_draft_attachments')
|
||||
|
||||
@@ -292,7 +292,7 @@ class App.TicketCreate extends App.Controller
|
||||
localeRender = =>
|
||||
@render(template)
|
||||
App.QueueManager.add(@queueKey, localeRender)
|
||||
return if !@formMeta
|
||||
return if !@formMeta && !@controllerFormCreateMiddle
|
||||
App.QueueManager.run(@queueKey)
|
||||
|
||||
importDraftAttachments: (options = {}) =>
|
||||
@@ -332,6 +332,10 @@ class App.TicketCreate extends App.Controller
|
||||
if !_.isEmpty(params['form_id'])
|
||||
@formId = params['form_id']
|
||||
|
||||
if !_.isEmpty(params)
|
||||
# only use form meta once so it will not get used on templates
|
||||
@formMeta = undefined
|
||||
|
||||
params.priority_id ||= App.TicketPriority.findByAttribute( 'default_create', true )?.id
|
||||
|
||||
@html(App.view('agent_ticket_create')(
|
||||
|
||||
@@ -20,6 +20,7 @@ class App.KnowledgeBaseSidebarLinkedTickets extends App.Controller
|
||||
render: ->
|
||||
@html App.view('knowledge_base/sidebar/linked_tickets')(
|
||||
tickets: @object.translation(@kb_locale.id)?.linked_tickets() || []
|
||||
editable: true
|
||||
)
|
||||
|
||||
@renderPopovers()
|
||||
|
||||
@@ -10,6 +10,7 @@ class App.KnowledgeBaseSidebarTags extends App.Controller
|
||||
object_type: 'KnowledgeBaseAnswer'
|
||||
object: @object
|
||||
tags: @object.tags
|
||||
editable: true
|
||||
)
|
||||
|
||||
updateIfNeeded: ->
|
||||
|
||||
@@ -24,6 +24,7 @@ App.Config.set('manage/:target', ManageRouter, 'Routes')
|
||||
App.Config.set('manage/:target/:page', ManageRouter, 'Routes')
|
||||
App.Config.set('settings/:target', ManageRouter, 'Routes')
|
||||
App.Config.set('channels/:target', ManageRouter, 'Routes')
|
||||
App.Config.set('channels/:target/error/:error_code', ManageRouter, 'Routes')
|
||||
App.Config.set('channels/:target/:channel_id', ManageRouter, 'Routes')
|
||||
App.Config.set('system/:target', ManageRouter, 'Routes')
|
||||
App.Config.set('system/:target/:integration', ManageRouter, 'Routes')
|
||||
|
||||
@@ -15,8 +15,9 @@ class Package extends App.ControllerSubContent
|
||||
url: "#{@apiPath}/packages",
|
||||
processData: true,
|
||||
success: (data) =>
|
||||
@packages = data.packages
|
||||
@commands = data.commands
|
||||
@packages = data.packages
|
||||
@package_installation = data.package_installation
|
||||
@local_gemfiles = data.local_gemfiles
|
||||
@render()
|
||||
)
|
||||
|
||||
@@ -35,7 +36,8 @@ class Package extends App.ControllerSubContent
|
||||
@html App.view('package')(
|
||||
head: __('Dashboard')
|
||||
packages: @packages
|
||||
commands: @commands
|
||||
package_installation: @package_installation
|
||||
local_gemfiles: @local_gemfiles
|
||||
)
|
||||
|
||||
action: (e) ->
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class App.Search extends App.Controller
|
||||
@extend App.PopoverProvidable
|
||||
@extend App.TicketMassUpdatable
|
||||
|
||||
elements:
|
||||
'.js-search': 'searchInput'
|
||||
@@ -11,6 +12,8 @@ class App.Search extends App.Controller
|
||||
'click .js-tab': 'showTab'
|
||||
'input .js-search': 'updateFilledClass'
|
||||
|
||||
@include App.ValidUsersForTicketSelectionMethods
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
@@ -36,6 +39,11 @@ class App.Search extends App.Controller
|
||||
@render()
|
||||
)
|
||||
|
||||
load = (data) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@formMeta = data.form_meta
|
||||
@bindId = App.TicketOverviewCollection.bind(load)
|
||||
|
||||
meta: =>
|
||||
title = @query || App.i18n.translateInline('Extended Search')
|
||||
|
||||
@@ -54,9 +62,13 @@ class App.Search extends App.Controller
|
||||
if @table
|
||||
@table.show()
|
||||
@navupdate(url: '#search', type: 'menu')
|
||||
return if _.isEmpty(params.query)
|
||||
|
||||
@$('.js-search').val(params.query).trigger('keyup')
|
||||
if !_.isEmpty(params.query)
|
||||
@$('.js-search').val(params.query).trigger('keyup')
|
||||
return
|
||||
|
||||
if @query
|
||||
@search(500, true)
|
||||
|
||||
hide: ->
|
||||
if @table
|
||||
@@ -90,11 +102,22 @@ class App.Search extends App.Controller
|
||||
@tabs.push tab
|
||||
|
||||
# build view
|
||||
@html App.view('search/index')(
|
||||
elLocal = $(App.view('search/index')(
|
||||
query: @query
|
||||
tabs: @tabs
|
||||
))
|
||||
|
||||
@controllerTicketBatch.releaseController() if @controllerTicketBatch
|
||||
@controllerTicketBatch = new App.TicketBatch(
|
||||
el: elLocal.filter('.js-batch-overlay')
|
||||
parent: @
|
||||
parentEl: elLocal
|
||||
appEl: @appEl
|
||||
batchSuccess: =>
|
||||
@search(0, true)
|
||||
)
|
||||
|
||||
@html elLocal
|
||||
if @query
|
||||
@search(500, true)
|
||||
|
||||
@@ -133,6 +156,7 @@ class App.Search extends App.Controller
|
||||
@globalSearch.search(
|
||||
delay: delay
|
||||
query: @query
|
||||
force: force
|
||||
)
|
||||
|
||||
renderResult: (result = []) =>
|
||||
@@ -229,16 +253,16 @@ class App.Search extends App.Controller
|
||||
|
||||
updateSearch = =>
|
||||
callback = =>
|
||||
@search(true)
|
||||
@search(0, true)
|
||||
@delay(callback, 100)
|
||||
|
||||
@bulkForm.releaseController() if @bulkForm
|
||||
@bulkForm = new App.TicketBulkForm(
|
||||
el: @el.find('.bulkAction')
|
||||
holder: localeEl
|
||||
view: @view
|
||||
callback: updateSearch
|
||||
noSidebar: true
|
||||
el: @el.find('.bulkAction')
|
||||
holder: localeEl
|
||||
view: @view
|
||||
batchSuccess: updateSearch
|
||||
noSidebar: true
|
||||
)
|
||||
|
||||
# start bulk action observ
|
||||
|
||||
@@ -1,522 +1,22 @@
|
||||
class App.TicketOverview extends App.Controller
|
||||
@extend App.TicketMassUpdatable
|
||||
@include App.ValidUsersForTicketSelectionMethods
|
||||
|
||||
className: 'overviews'
|
||||
activeFocus: 'nav'
|
||||
mouse:
|
||||
x: null
|
||||
y: null
|
||||
batchAnimationPaused: false
|
||||
|
||||
elements:
|
||||
'.js-batch-overlay': 'batchOverlay'
|
||||
'.js-batch-overlay-backdrop': 'batchOverlayBackdrop'
|
||||
'.js-batch-cancel': 'batchCancel'
|
||||
'.js-batch-macro-circle': 'batchMacroCircle'
|
||||
'.js-batch-assign-circle': 'batchAssignCircle'
|
||||
'.js-batch-assign': 'batchAssign'
|
||||
'.js-batch-assign-inner': 'batchAssignInner'
|
||||
'.js-batch-assign-group': 'batchAssignGroup'
|
||||
'.js-batch-assign-group-name': 'batchAssignGroupName'
|
||||
'.js-batch-assign-group-inner': 'batchAssignGroupInner'
|
||||
'.js-batch-macro': 'batchMacro'
|
||||
'.main': 'mainContent'
|
||||
|
||||
events:
|
||||
'mousedown .item': 'startDragItem'
|
||||
'mouseenter .js-batch-hover-target': 'highlightBatchEntry'
|
||||
'mouseleave .js-batch-hover-target': 'unhighlightBatchEntry'
|
||||
|
||||
@include App.ValidUsersForTicketSelectionMethods
|
||||
'.main': 'mainContent'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@batchSupport = @permissionCheck('ticket.agent')
|
||||
@render()
|
||||
|
||||
# rerender view, e. g. on language change
|
||||
@controllerBind('ui:rerender', =>
|
||||
@renderBatchOverlay()
|
||||
)
|
||||
load = (data) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
@formMeta = data.form_meta
|
||||
@bindId = App.TicketOverviewCollection.bind(load)
|
||||
|
||||
startDragItem: (event) =>
|
||||
return if !@batchSupport
|
||||
@grabbedItem = $(event.currentTarget)
|
||||
offset = @grabbedItem.offset()
|
||||
@batchDragger = $(App.view('ticket_overview/batch_dragger')())
|
||||
@grabbedItemClone = @grabbedItem.clone()
|
||||
@grabbedItemClone.data('offset', @grabbedItem.offset())
|
||||
@grabbedItemClone.addClass('batch-dragger-item js-main-item')
|
||||
@batchDragger.append @grabbedItemClone
|
||||
|
||||
@batchDragger.data
|
||||
startX: event.pageX
|
||||
startY: event.pageY
|
||||
dx: Math.min(event.pageX - offset.left, 180)
|
||||
dy: event.pageY - offset.top
|
||||
moved: false
|
||||
|
||||
$(document).on 'mousemove.item', @dragItem
|
||||
$(document).one 'mouseup.item', @endDragItem
|
||||
# TODO: fire @cancelDrag on ESC
|
||||
|
||||
dragItem: (event) =>
|
||||
pos = @batchDragger.data()
|
||||
threshold = 3
|
||||
x = event.pageX - pos.dx
|
||||
y = event.pageY - pos.dy
|
||||
dir = if event.pageY > pos.startY then 1 else -1
|
||||
|
||||
if !pos.moved
|
||||
if Math.abs(event.pageY - pos.startY) > threshold || Math.abs(event.pageX - pos.startX) > threshold
|
||||
@batchDragger.data 'moved', true
|
||||
@el.addClass('u-no-userselect')
|
||||
# check grabbed items batch checkbox to make sure its checked
|
||||
# (could be grabbed without checking the checkbox it)
|
||||
@grabbedItemWasntChecked = !@grabbedItem.find('[name="bulk"]').prop('checked')
|
||||
@grabbedItem.find('[name="bulk"]').prop('checked', true)
|
||||
@grabbedItemClone.find('[name="bulk"]').prop('checked', true)
|
||||
|
||||
additionalItems = @el.find('[name="bulk"]:checked').parents('.item').not(@grabbedItem)
|
||||
additionalItemsClones = additionalItems.clone()
|
||||
@draggedItems = @grabbedItemClone.add(additionalItemsClones)
|
||||
# store offsets for later use
|
||||
additionalItemsClones.each (i, item) -> $(@).data('offset', additionalItems.eq(i).offset())
|
||||
@batchDragger.prepend additionalItemsClones.addClass('batch-dragger-item').get().reverse()
|
||||
if(additionalItemsClones.length)
|
||||
@batchDragger.find('.js-batch-dragger-count').text(@draggedItems.length)
|
||||
|
||||
@renderOptions()
|
||||
|
||||
@appEl.append(@batchDragger)
|
||||
|
||||
@draggedItems.each (i, item) ->
|
||||
dx = $(item).data('offset').left - $(item).offset().left - x
|
||||
dy = $(item).data('offset').top - $(item).offset().top - y
|
||||
$.Velocity.hook item, 'translateX', "#{dx}px"
|
||||
$.Velocity.hook item, 'translateY', "#{dy}px"
|
||||
|
||||
@alignDraggedItems(-dir)
|
||||
|
||||
@mouseY = event.pageY
|
||||
@showBatchOverlay()
|
||||
else
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
$.Velocity.hook @batchDragger, 'translateX', "#{x}px"
|
||||
$.Velocity.hook @batchDragger, 'translateY', "#{y}px"
|
||||
|
||||
endDragItem: (event) =>
|
||||
$(document).off 'mousemove.item'
|
||||
$(document).off 'mouseup.item'
|
||||
pos = @batchDragger.data()
|
||||
|
||||
@clearDelay('clear-hovered-batch-entry')
|
||||
|
||||
if !@hoveredBatchEntry
|
||||
@cleanUpDrag()
|
||||
return
|
||||
|
||||
$.Velocity.hook @batchDragger, 'transformOriginX', "#{pos.dx}px"
|
||||
$.Velocity.hook @batchDragger, 'transformOriginY', "#{pos.dy}px"
|
||||
@hoveredBatchEntry.velocity
|
||||
properties:
|
||||
scale: 1.1
|
||||
options:
|
||||
duration: 200
|
||||
complete: =>
|
||||
if !@hoveredBatchEntry
|
||||
@cleanUpDrag()
|
||||
return
|
||||
|
||||
@hoveredBatchEntry.velocity 'reverse',
|
||||
duration: 200
|
||||
complete: =>
|
||||
|
||||
if !@hoveredBatchEntry
|
||||
@cleanUpDrag()
|
||||
return
|
||||
|
||||
# clean scale
|
||||
action = @hoveredBatchEntry.attr('data-action')
|
||||
id = @hoveredBatchEntry.attr('data-id')
|
||||
groupId = @hoveredBatchEntry.attr('data-group-id')
|
||||
items = @el.find('[name="bulk"]:checked')
|
||||
@hoveredBatchEntry.removeAttr('style')
|
||||
@cleanUpDrag(true)
|
||||
|
||||
@performBatchAction items, action, id, groupId
|
||||
@batchDragger.velocity
|
||||
properties:
|
||||
scale: 0
|
||||
options:
|
||||
duration: 200
|
||||
|
||||
cancelDrag: ->
|
||||
$(document).off 'mousemove.item'
|
||||
$(document).off 'mouseup.item'
|
||||
@cleanUpDrag()
|
||||
|
||||
cleanUpDrag: (success) ->
|
||||
@hideBatchOverlay()
|
||||
@el.removeClass('u-no-userselect')
|
||||
$('.batch-dragger').remove()
|
||||
@hoveredBatchEntry = null
|
||||
|
||||
if @grabbedItemWasntChecked
|
||||
@grabbedItem.find('[name="bulk"]').prop('checked', false)
|
||||
|
||||
if success
|
||||
# uncheck all checked items
|
||||
@el.find('[name="bulk"]:checked').prop('checked', false)
|
||||
@el.find('[name="bulk_all"]').prop('checked', false)
|
||||
|
||||
alignDraggedItems: (dir) ->
|
||||
@draggedItems.velocity
|
||||
properties:
|
||||
translateX: 0
|
||||
translateY: (i) => dir * i * @batchDragger.height()/2
|
||||
options:
|
||||
easing: 'ease-in-out'
|
||||
duration: 300
|
||||
|
||||
@batchDragger.find('.js-batch-dragger-count').velocity
|
||||
properties:
|
||||
translateY: if dir < 0 then 0 else -@batchDragger.height()+8
|
||||
options:
|
||||
easing: 'ease-in-out'
|
||||
duration: 300
|
||||
|
||||
performBatchAction: (items, action, id, groupId) ->
|
||||
ticket_ids = items.toArray().map (item) -> $(item).val()
|
||||
|
||||
switch action
|
||||
when 'macro'
|
||||
path = 'macro'
|
||||
data =
|
||||
ticket_ids: ticket_ids
|
||||
macro_id: id
|
||||
|
||||
when 'user_assign'
|
||||
path = 'update'
|
||||
|
||||
data =
|
||||
ticket_ids: ticket_ids
|
||||
attributes:
|
||||
owner_id: id
|
||||
|
||||
if !_.isEmpty(groupId)
|
||||
data.attributes.group_id = groupId
|
||||
|
||||
when 'group_assign'
|
||||
path = 'update'
|
||||
|
||||
data =
|
||||
ticket_ids: ticket_ids
|
||||
attributes:
|
||||
group_id: id
|
||||
|
||||
@ajax_mass(path, data)
|
||||
|
||||
showBatchOverlay: ->
|
||||
@batchOverlay.addClass('is-visible')
|
||||
$('html').css('overflow', 'hidden')
|
||||
@batchOverlayBackdrop.velocity { opacity: [1, 0] }, { duration: 500 }
|
||||
@batchMacroOffset = @batchMacro.offset().top + @batchMacro.outerHeight()
|
||||
@batchAssignOffset = @batchAssign.offset().top
|
||||
@batchOverlayShown = true
|
||||
$(document).on 'mousemove.batchoverlay', @controlBatchOverlay
|
||||
|
||||
hideBatchOverlay: ->
|
||||
$(document).off 'mousemove.batchoverlay'
|
||||
@batchOverlayShown = false
|
||||
@batchOverlayBackdrop.velocity { opacity: [0, 1] }, { duration: 300, queue: false }
|
||||
@hideBatchCircles =>
|
||||
@batchOverlay.removeClass('is-visible')
|
||||
|
||||
$('html').css('overflow', '')
|
||||
|
||||
if @batchAssignShown
|
||||
@hideBatchAssign()
|
||||
|
||||
if @batchMacroShown
|
||||
@hideBatchMacro()
|
||||
|
||||
if @batchAssignGroupShown
|
||||
@hideBatchAssignGroup()
|
||||
|
||||
controlBatchOverlay: (event) =>
|
||||
return if @batchAnimationPaused
|
||||
# store to detect if the mouse is hovering a drag-action entry
|
||||
# after an animation ended -> @highlightBatchEntryAtMousePosition
|
||||
@mouse.x = event.pageX
|
||||
@mouse.y = event.pageY
|
||||
|
||||
if @batchAssignGroupShown && @batchAssignGroupOffset != undefined
|
||||
if @mouse.y < @batchAssignGroupOffset
|
||||
@hideBatchAssignGroup()
|
||||
@batchAnimationPaused = true
|
||||
return
|
||||
|
||||
if @mouse.y <= @batchMacroOffset
|
||||
mouseInArea = 'top'
|
||||
else if @mouse.y > @batchMacroOffset && @mouse.y <= @batchAssignOffset
|
||||
mouseInArea = 'middle'
|
||||
else
|
||||
mouseInArea = 'bottom'
|
||||
|
||||
switch mouseInArea
|
||||
when 'top'
|
||||
if !@batchMacroShown
|
||||
@hideBatchCircles()
|
||||
@showBatchMacro()
|
||||
@alignDraggedItems(1)
|
||||
|
||||
when 'middle'
|
||||
if @batchAssignShown
|
||||
@hideBatchAssign()
|
||||
|
||||
if @batchMacroShown
|
||||
@hideBatchMacro()
|
||||
|
||||
if !@batchCirclesShown
|
||||
@showBatchCircles()
|
||||
|
||||
when 'bottom'
|
||||
if !@batchAssignShown
|
||||
@hideBatchCircles()
|
||||
@showBatchAssign()
|
||||
@alignDraggedItems(-1)
|
||||
|
||||
showBatchCircles: ->
|
||||
@batchCirclesShown = true
|
||||
|
||||
@batchMacroCircle.velocity
|
||||
properties:
|
||||
translateY: [0, '-150%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
delay: 200
|
||||
|
||||
@batchAssignCircle.velocity
|
||||
properties:
|
||||
translateY: [0, '150%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
delay: 200
|
||||
|
||||
hideBatchCircles: (callback) ->
|
||||
@batchMacroCircle.velocity
|
||||
properties:
|
||||
translateY: ['-150%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchAssignCircle.velocity
|
||||
properties:
|
||||
translateY: ['150%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
complete: callback
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchCirclesShown = false
|
||||
|
||||
showBatchAssign: ->
|
||||
return if !@batchOverlayShown # user might have dropped the item already
|
||||
@batchAssignShown = true
|
||||
|
||||
@batchCancel.css
|
||||
top: 0
|
||||
bottom: @batchAssign.height()
|
||||
|
||||
@batchAssign.velocity
|
||||
properties:
|
||||
translateY: [0, '100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
complete: @highlightBatchEntryAtMousePosition
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: [0, '100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
|
||||
hideBatchAssign: ->
|
||||
@batchAssign.velocity
|
||||
properties:
|
||||
translateY: ['100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
complete: =>
|
||||
$.Velocity.hook @batchAssign, 'translateY', '0%'
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: ['100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchAssignShown = false
|
||||
|
||||
showBatchAssignGroup: =>
|
||||
return if !@batchOverlayShown # user might have dropped the item already
|
||||
@batchAssignGroupShown = true
|
||||
|
||||
groupId = @hoveredBatchEntry.attr('data-id')
|
||||
group = App.Group.find(groupId)
|
||||
|
||||
@batchAssignGroupName.text group.displayName()
|
||||
@batchAssignGroupInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
|
||||
users: @usersInGroups([groupId])
|
||||
groups: []
|
||||
groupId: groupId
|
||||
))
|
||||
|
||||
# then adjust the size of the group that it almost overlaps the batch-assign box
|
||||
@batchAssignGroupInner.height(@batchAssignInner.height())
|
||||
|
||||
@batchAssignGroup.velocity
|
||||
properties:
|
||||
translateY: [0, '100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 700
|
||||
visibility: 'visible'
|
||||
complete: =>
|
||||
@highlightBatchEntryAtMousePosition()
|
||||
@batchAssignGroupOffset = @batchAssignGroup.offset().top
|
||||
|
||||
hideBatchAssignGroup: ->
|
||||
@batchAssignGroup.velocity
|
||||
properties:
|
||||
translateY: ['100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
complete: =>
|
||||
@batchAssignGroupShown = false
|
||||
@batchAssignGroupHovered = false
|
||||
setTimeout (=> @batchAnimationPaused = false), 1000
|
||||
|
||||
@batchAssignGroupOffset = undefined
|
||||
|
||||
showBatchMacro: ->
|
||||
return if !@batchOverlayShown # user might have dropped the item already
|
||||
@batchMacroShown = true
|
||||
|
||||
@batchCancel.css
|
||||
bottom: 0
|
||||
top: @batchMacro.height()
|
||||
|
||||
@batchMacro.velocity
|
||||
properties:
|
||||
translateY: [0, '-100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
complete: @highlightBatchEntryAtMousePosition
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: [0, '-100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
|
||||
hideBatchMacro: ->
|
||||
@batchMacro.velocity
|
||||
properties:
|
||||
translateY: ['-100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
complete: =>
|
||||
$.Velocity.hook @batchMacro, 'translateY', '0%'
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: ['-100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchMacroShown = false
|
||||
|
||||
highlightBatchEntryAtMousePosition: =>
|
||||
entryAtPoint = $(document.elementFromPoint(@mouse.x, @mouse.y)).closest('.js-batch-overlay-entry .avatar')
|
||||
if(entryAtPoint.length)
|
||||
@hoveredBatchEntry = entryAtPoint.closest('.js-batch-overlay-entry').addClass('is-hovered')
|
||||
|
||||
highlightBatchEntry: (event) ->
|
||||
@clearDelay('clear-hovered-batch-entry')
|
||||
@hoveredBatchEntry = $(event.currentTarget).closest('.js-batch-overlay-entry').addClass('is-hovered')
|
||||
|
||||
if @hoveredBatchEntry.attr('data-action') is 'group_assign'
|
||||
@batchAssignGroupHintTimeout = setTimeout @blinkBatchEntry, 800
|
||||
@batchAssignGroupTimeout = setTimeout @showBatchAssignGroup, 900
|
||||
|
||||
unhighlightBatchEntry: (event) ->
|
||||
return if !@hoveredBatchEntry
|
||||
if @hoveredBatchEntry.attr('data-action') is 'group_assign'
|
||||
if @batchAssignGroupTimeout
|
||||
clearTimeout @batchAssignGroupTimeout
|
||||
if @batchAssignGroupHintTimeout
|
||||
clearTimeout @batchAssignGroupHintTimeout
|
||||
|
||||
@hoveredBatchEntry.removeClass('is-hovered')
|
||||
delay = =>
|
||||
@hoveredBatchEntry = null
|
||||
@delay(delay, 800, 'clear-hovered-batch-entry')
|
||||
|
||||
blinkBatchEntry: =>
|
||||
@hoveredBatchEntry
|
||||
.velocity({ opacity: [0.5, 1] }, { duration: 120 })
|
||||
.velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
|
||||
.velocity({ opacity: [0.5, 1] }, { duration: 120 })
|
||||
.velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
|
||||
|
||||
render: ->
|
||||
elLocal = $(App.view('ticket_overview/index')())
|
||||
|
||||
@@ -533,6 +33,16 @@ class App.TicketOverview extends App.Controller
|
||||
view: @view
|
||||
)
|
||||
|
||||
@controllerTicketBatch.releaseController() if @controllerTicketBatch
|
||||
@controllerTicketBatch = new App.TicketBatch(
|
||||
el: elLocal.filter('.js-batch-overlay')
|
||||
parent: @
|
||||
parentEl: elLocal
|
||||
appEl: @appEl
|
||||
batchSuccess: =>
|
||||
@render()
|
||||
)
|
||||
|
||||
@contentController.releaseController() if @contentController
|
||||
@contentController = new Table(
|
||||
el: elLocal.find('.overview-table')
|
||||
@@ -541,8 +51,6 @@ class App.TicketOverview extends App.Controller
|
||||
keyboardOff: @keyboardOff
|
||||
)
|
||||
|
||||
@renderBatchOverlay(elLocal.filter('.js-batch-overlay'))
|
||||
|
||||
@html elLocal
|
||||
|
||||
@$('.main').on('click', =>
|
||||
@@ -559,54 +67,6 @@ class App.TicketOverview extends App.Controller
|
||||
@delay(update, 2800, 'overview:fetch')
|
||||
)
|
||||
|
||||
renderBatchOverlay: (elLocal) =>
|
||||
if elLocal
|
||||
elLocal.html( App.view('ticket_overview/batch_overlay')() )
|
||||
return
|
||||
@batchOverlay.html( App.view('ticket_overview/batch_overlay')() )
|
||||
@refreshElements()
|
||||
|
||||
renderOptions: =>
|
||||
@renderOptionsGroups()
|
||||
@renderOptionsMacros()
|
||||
|
||||
renderOptionsGroups: =>
|
||||
@batchAssignInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
|
||||
@validUsersForTicketSelection()
|
||||
))
|
||||
|
||||
renderOptionsMacros: =>
|
||||
|
||||
@possibleMacros = []
|
||||
macros = App.Macro.getList()
|
||||
|
||||
items = @el.find('[name="bulk"]:checked')
|
||||
|
||||
group_ids =[]
|
||||
for item in items
|
||||
ticket = App.Ticket.find($(item).val())
|
||||
group_ids.push ticket.group_id
|
||||
|
||||
group_ids = _.uniq(group_ids)
|
||||
|
||||
for macro in macros
|
||||
|
||||
# push if no group_ids exists
|
||||
if _.isEmpty(macro.group_ids) && !_.includes(@possibleMacros, macro)
|
||||
@possibleMacros.push macro
|
||||
|
||||
# push if group_ids are equal
|
||||
if _.isEqual(macro.group_ids, group_ids) && !_.includes(@possibleMacros, macro)
|
||||
@possibleMacros.push macro
|
||||
|
||||
# push if all group_ids of tickets are in macro.group_ids
|
||||
if !_.isEmpty(macro.group_ids) && _.isEmpty(_.difference(group_ids,macro.group_ids)) && !_.includes(@possibleMacros, macro)
|
||||
@possibleMacros.push macro
|
||||
|
||||
@batchMacro.html $(App.view('ticket_overview/batch_overlay_macro')(
|
||||
macros: @possibleMacros
|
||||
))
|
||||
|
||||
active: (state) =>
|
||||
return @shown if state is undefined
|
||||
@shown = state
|
||||
@@ -1047,6 +507,8 @@ class Table extends App.Controller
|
||||
|
||||
# open ticket via task manager to provide task with overview info
|
||||
ticket = App.Ticket.findNative(id)
|
||||
return if !ticket
|
||||
|
||||
App.TaskManager.execute(
|
||||
key: "Ticket-#{ticket.id}"
|
||||
controller: 'TicketZoom'
|
||||
@@ -1217,9 +679,11 @@ class Table extends App.Controller
|
||||
|
||||
@bulkForm.releaseController() if @bulkForm
|
||||
@bulkForm = new App.TicketBulkForm(
|
||||
el: @el.find('.bulkAction')
|
||||
holder: @el
|
||||
view: @view
|
||||
el: @el.find('.bulkAction')
|
||||
holder: @el
|
||||
view: @view
|
||||
batchSuccess: =>
|
||||
@render()
|
||||
)
|
||||
|
||||
# start bulk action observ
|
||||
|
||||
@@ -93,7 +93,7 @@ class App.TicketZoom extends App.Controller
|
||||
@ajax(
|
||||
id: "ticket_zoom_#{@ticket_id}"
|
||||
type: 'GET'
|
||||
url: "#{@apiPath}/tickets/#{@ticket_id}?all=true"
|
||||
url: "#{@apiPath}/tickets/#{@ticket_id}?all=true&auto_assign=true"
|
||||
processData: true
|
||||
queue: queue
|
||||
success: (data, status, xhr) =>
|
||||
@@ -335,7 +335,7 @@ class App.TicketZoom extends App.Controller
|
||||
|
||||
hide: =>
|
||||
@activeState = false
|
||||
$('body > .modal').modal('hide')
|
||||
$('body > .modal').modal('hide') if @shown
|
||||
@positionPageHeaderStop()
|
||||
@autosaveStop()
|
||||
@shortcutNavigationstop()
|
||||
@@ -557,26 +557,6 @@ class App.TicketZoom extends App.Controller
|
||||
links: @links
|
||||
)
|
||||
|
||||
# check if autolock is needed
|
||||
if @Config.get('ticket_auto_assignment') is true
|
||||
if @ticket.owner_id is 1 && @permissionCheck('ticket.agent') && @ticket.editable('full')
|
||||
userIdsIgnore = @Config.get('ticket_auto_assignment_user_ids_ignore') || []
|
||||
if !_.isArray(userIdsIgnore)
|
||||
userIdsIgnore = [userIdsIgnore]
|
||||
userIgnored = false
|
||||
currentUserId = App.Session.get('id')
|
||||
for userIdIgnore in userIdsIgnore
|
||||
if userIdIgnore.toString() is currentUserId.toString()
|
||||
userIgnored = true
|
||||
break
|
||||
if userIgnored is false
|
||||
ticket_auto_assignment_selector = @Config.get('ticket_auto_assignment_selector')
|
||||
if App.Ticket.selector(@ticket, ticket_auto_assignment_selector['condition'])
|
||||
assign = =>
|
||||
@ticket.owner_id = App.Session.get('id')
|
||||
@ticket.save()
|
||||
@delay(assign, 800, "ticket-auto-assign-#{@ticket.id}")
|
||||
|
||||
# render init content
|
||||
if elLocal
|
||||
@html elLocal
|
||||
@@ -979,6 +959,8 @@ class App.TicketZoom extends App.Controller
|
||||
new_article: @articleNew.params()
|
||||
ticket_attributes: @ticketParams()
|
||||
|
||||
params.new_article.body = App.Utils.signatureRemoveByHtml(params.new_article.body)
|
||||
|
||||
loaded_draft_id = params.new_article.shared_draft_id
|
||||
|
||||
params.form_id = params.new_article['form_id']
|
||||
@@ -1226,6 +1208,41 @@ class TicketZoomRouter extends App.ControllerPermanent
|
||||
constructor: (params) ->
|
||||
super
|
||||
|
||||
return @byNumber(params) if params.ticket_number
|
||||
@byTicketId(params)
|
||||
|
||||
byNumber: (params) ->
|
||||
return @byTicketId(params) if !params.ticket_number
|
||||
return @byTicketId(params) if params.ticket_id
|
||||
|
||||
number = params.ticket_number
|
||||
delete params.ticket_number
|
||||
|
||||
ticket = App.Ticket.findByAttribute('number', number)
|
||||
return @navigate("ticket/zoom/#{ticket.id}") if ticket
|
||||
|
||||
App.Ajax.request(
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/tickets/search"
|
||||
processData: true
|
||||
data: JSON.stringify(
|
||||
condition: {
|
||||
'ticket.number': {
|
||||
operator: 'is',
|
||||
value: number
|
||||
}
|
||||
}
|
||||
limit: 1
|
||||
)
|
||||
success: (data, status, xhr) =>
|
||||
return @byTicketId(params) if _.isEmpty(data.tickets)
|
||||
@navigate("ticket/zoom/#{data.tickets[0]}")
|
||||
error: =>
|
||||
@byTicketId(params)
|
||||
)
|
||||
|
||||
byTicketId: (params) ->
|
||||
|
||||
# cleanup params
|
||||
clean_params =
|
||||
ticket_id: params.ticket_id
|
||||
@@ -1240,6 +1257,7 @@ class TicketZoomRouter extends App.ControllerPermanent
|
||||
show: true
|
||||
)
|
||||
|
||||
App.Config.set('ticket/zoom/number/:ticket_number', TicketZoomRouter, 'Routes')
|
||||
App.Config.set('ticket/zoom/:ticket_id', TicketZoomRouter, 'Routes')
|
||||
App.Config.set('ticket/zoom/:ticket_id/nav/:nav', TicketZoomRouter, 'Routes')
|
||||
App.Config.set('ticket/zoom/:ticket_id/:article_id', TicketZoomRouter, 'Routes')
|
||||
|
||||
@@ -18,9 +18,12 @@ class App.TicketZoomArticleActions extends App.Controller
|
||||
@html ''
|
||||
|
||||
actionRow: (ticket, article) ->
|
||||
ticket = App.Ticket.fullLocal(ticket.id)
|
||||
article = App.TicketArticle.fullLocal(article.id)
|
||||
actionConfig = App.Config.get('TicketZoomArticleAction')
|
||||
keys = _.keys(actionConfig).sort()
|
||||
actions = []
|
||||
keys = _.keys(actionConfig).sort()
|
||||
actions = []
|
||||
|
||||
for key in keys
|
||||
config = actionConfig[key]
|
||||
if config
|
||||
|
||||
@@ -5,6 +5,7 @@ class App.TicketZoomArticleImageView extends App.ControllerModal
|
||||
buttonClass: 'btn--success'
|
||||
head: ''
|
||||
veryLarge: true
|
||||
nextElement: null
|
||||
|
||||
events:
|
||||
'submit form': 'submit'
|
||||
@@ -20,16 +21,14 @@ class App.TicketZoomArticleImageView extends App.ControllerModal
|
||||
)
|
||||
|
||||
nextRight: =>
|
||||
nextElement = @parentElement.closest('.attachment').next('.attachment.attachment--preview')
|
||||
return if nextElement.length is 0
|
||||
@nextElement = @parentElement.closest('.attachment').next('.attachment.attachment--preview')
|
||||
return if @nextElement.length is 0
|
||||
@close()
|
||||
nextElement.find('img').trigger('click')
|
||||
|
||||
nextLeft: =>
|
||||
prevElement = @parentElement.closest('.attachment').prev('.attachment.attachment--preview')
|
||||
return if prevElement.length is 0
|
||||
@nextElement = @parentElement.closest('.attachment').prev('.attachment.attachment--preview')
|
||||
return if @nextElement.length is 0
|
||||
@close()
|
||||
prevElement.find('img').trigger('click')
|
||||
|
||||
content: ->
|
||||
@image = @image.replace(/view=preview/, 'view=inline')
|
||||
@@ -40,8 +39,15 @@ class App.TicketZoomArticleImageView extends App.ControllerModal
|
||||
url = "#{$(@image).attr('src')}?disposition=attachment"
|
||||
window.open(url, '_blank')
|
||||
|
||||
onShow: =>
|
||||
@el.attr('tabindex', '-1')
|
||||
$('.modal').focus()
|
||||
|
||||
onClose: =>
|
||||
@unbindAll()
|
||||
|
||||
onClosed: =>
|
||||
@nextElement.find('img').trigger('click') if @nextElement
|
||||
|
||||
unbindAll: ->
|
||||
$(document).off('keydown.image_preview')
|
||||
|
||||
@@ -55,11 +55,13 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||
|
||||
@openTextarea(null, true, !data.nofocus)
|
||||
for key, value of data.article
|
||||
if key is 'body'
|
||||
@$("[data-name=\"#{key}\"]").html(value)
|
||||
else
|
||||
@$("[name=\"#{key}\"]").val(value).trigger('change')
|
||||
|
||||
switch key
|
||||
when 'body'
|
||||
@$("[data-name=\"#{key}\"]").html(value)
|
||||
when 'internal'
|
||||
@setArticleInternal(value)
|
||||
else
|
||||
@$("[name=\"#{key}\"]").val(value).trigger('change')
|
||||
|
||||
@$('[name=shared_draft_id]').val(data.shared_draft_id)
|
||||
|
||||
@@ -363,19 +365,13 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||
@el.find('.js-articleTypes').addClass('is-hidden')
|
||||
|
||||
setArticleInternal: (internal) =>
|
||||
if internal is true
|
||||
@articleNewEdit
|
||||
.removeClass('is-public')
|
||||
.addClass('is-internal')
|
||||
|
||||
@$('[name=internal]').val('true')
|
||||
return
|
||||
|
||||
@articleNewEdit
|
||||
.addClass('is-public')
|
||||
.removeClass('is-internal')
|
||||
.toggleClass('is-public', !internal)
|
||||
.toggleClass('is-internal', internal)
|
||||
|
||||
@$('[name=internal]').val('')
|
||||
value = if internal then 'true' else ''
|
||||
|
||||
@$('[name=internal]').val(value)
|
||||
|
||||
setArticleTypePre: (type, signaturePosition = 'bottom') =>
|
||||
wasScrolledToBottom = @isScrolledToBottom()
|
||||
|
||||
@@ -94,7 +94,7 @@ class ArticleViewItem extends App.ControllerObserver
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@seeMore = false
|
||||
@seeMoreOpen = false
|
||||
|
||||
# set expand of text area only once
|
||||
@controllerBind('ui::ticket::shown', (data) =>
|
||||
@@ -254,12 +254,11 @@ class ArticleViewItem extends App.ControllerObserver
|
||||
bubbleOverflowContainer = @textBubbleOverflowContainer
|
||||
|
||||
# expand if see more is already clicked
|
||||
if @seeMore
|
||||
if @seeMoreOpen
|
||||
bubbleContent.css('height', 'auto')
|
||||
return
|
||||
|
||||
# reset bubble height and "see more" opacity
|
||||
bubbleContent.css('height', '')
|
||||
else
|
||||
# reset bubble height and "see more" opacity
|
||||
bubbleContent.css('height', '')
|
||||
bubbleOverflowContainer.css('opacity', '')
|
||||
|
||||
# remember offset of "see more"
|
||||
@@ -296,7 +295,9 @@ class ArticleViewItem extends App.ControllerObserver
|
||||
else if bubbleContentHeight > maxHeight
|
||||
bubbleContent.attr('data-height', bubbleContentHeight + 30)
|
||||
bubbleContent.attr('data-height-origin', maxHeight)
|
||||
bubbleContent.css('height', "#{maxHeight}px")
|
||||
newHeight = if @seeMoreOpen then 'auto' else "#{maxHeight}px"
|
||||
bubbleContent.css('height', newHeight)
|
||||
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
|
||||
bubbleOverflowContainer.removeClass('hide')
|
||||
else
|
||||
bubbleOverflowContainer.addClass('hide')
|
||||
@@ -448,15 +449,15 @@ class ArticleViewItem extends App.ControllerObserver
|
||||
bubbleOverflowContainer = @textBubbleOverflowContainer
|
||||
|
||||
if @seeMoreOpen
|
||||
label = App.i18n.translateContent('See more')
|
||||
@label = App.i18n.translateContent('See more')
|
||||
height = bubbleContent.attr('data-height-origin')
|
||||
@seeMoreOpen = false
|
||||
else
|
||||
label = App.i18n.translateContent('See less')
|
||||
@label = App.i18n.translateContent('See less')
|
||||
height = bubbleContent.attr('data-height')
|
||||
@seeMoreOpen = true
|
||||
|
||||
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(label)
|
||||
bubbleOverflowContainer.toggleClass('is-open', @seeMoreOpen).find('.js-toggleFold').html(@label)
|
||||
|
||||
bubbleContent.velocity
|
||||
properties:
|
||||
|
||||
@@ -175,6 +175,7 @@ class App.FormHandlerCoreWorkflow
|
||||
)
|
||||
|
||||
form.find('[name="' + field + '"]').closest('.form-group').replaceWith(newElement)
|
||||
form.find('[name="' + field + '"]').closest('.form-group').find('.js-helpMessage').tooltip()
|
||||
|
||||
if shown
|
||||
ui.show(field, form)
|
||||
|
||||
@@ -152,12 +152,14 @@ class SidebarTicket extends App.Controller
|
||||
object_type: 'Ticket'
|
||||
object: @ticket
|
||||
tags: @tags
|
||||
editable: @ticket.editable()
|
||||
)
|
||||
@linkWidget = new App.WidgetLink.Ticket(
|
||||
el: localEl.filter('.js-links')
|
||||
object_type: 'Ticket'
|
||||
object: @ticket
|
||||
links: @links
|
||||
editable: @ticket.editable()
|
||||
)
|
||||
|
||||
if @permissionCheck('knowledge_base.*') and App.Config.get('kb_active')
|
||||
@@ -166,6 +168,7 @@ class SidebarTicket extends App.Controller
|
||||
object_type: 'Ticket'
|
||||
object: @ticket
|
||||
links: @links
|
||||
editable: @ticket.editable()
|
||||
)
|
||||
|
||||
@timeUnitWidget = new App.TicketZoomTimeUnit(
|
||||
|
||||
@@ -43,6 +43,7 @@ class App.WidgetLinkKbAnswer extends App.WidgetLink
|
||||
render: ->
|
||||
@html App.view('link/kb_answer')(
|
||||
list: @linksForRendering()
|
||||
editable: @editable
|
||||
)
|
||||
|
||||
@renderPopovers()
|
||||
|
||||
@@ -47,11 +47,13 @@ class App.WidgetLink.Ticket extends App.WidgetLink
|
||||
tickets: list[type].tickets
|
||||
object: 'Ticket'
|
||||
linkType: type
|
||||
editable: @editable
|
||||
) unless list[type].tickets.length == 0
|
||||
|
||||
# insert data
|
||||
@html App.view('link/ticket/list')(
|
||||
links: list
|
||||
editable: @editable
|
||||
)
|
||||
|
||||
@renderPopovers()
|
||||
|
||||
@@ -57,6 +57,8 @@ class App.WidgetSharedDraft extends App.Controller
|
||||
|
||||
return false if meta.name.trim() == ''
|
||||
|
||||
form.body = App.Utils.signatureRemoveByHtml(form.body)
|
||||
|
||||
JSON.stringify({
|
||||
name: meta.name
|
||||
group_id: form.group_id
|
||||
|
||||
@@ -51,8 +51,10 @@ class App.WidgetTag extends App.Controller
|
||||
render: =>
|
||||
return if @lastLocalTags && _.isEqual(@lastLocalTags, @localTags)
|
||||
@lastLocalTags = _.clone(@localTags)
|
||||
|
||||
@html App.view(@templateName)(
|
||||
tags: @localTags || [],
|
||||
editable: @editable
|
||||
)
|
||||
source = "#{App.Config.get('api_path')}/tag_search"
|
||||
@$('.js-newTagInput').autocomplete(
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
class App.TicketBatch extends App.Controller
|
||||
requiredPermission: 'ticket.agent'
|
||||
|
||||
mouse:
|
||||
x: null
|
||||
y: null
|
||||
batchAnimationPaused: false
|
||||
|
||||
elements:
|
||||
'.js-batch-overlay-backdrop': 'batchOverlayBackdrop'
|
||||
'.js-batch-cancel': 'batchCancel'
|
||||
'.js-batch-macro-circle': 'batchMacroCircle'
|
||||
'.js-batch-assign-circle': 'batchAssignCircle'
|
||||
'.js-batch-assign': 'batchAssign'
|
||||
'.js-batch-assign-inner': 'batchAssignInner'
|
||||
'.js-batch-assign-group': 'batchAssignGroup'
|
||||
'.js-batch-assign-group-name': 'batchAssignGroupName'
|
||||
'.js-batch-assign-group-inner': 'batchAssignGroupInner'
|
||||
'.js-batch-macro': 'batchMacro'
|
||||
|
||||
events:
|
||||
'mouseenter .js-batch-hover-target': 'highlightBatchEntry'
|
||||
'mouseleave .js-batch-hover-target': 'unhighlightBatchEntry'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
||||
# rerender view, e. g. on language change
|
||||
@controllerBind('ui:rerender', @render)
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
@html App.view('ticket_overview/batch_overlay')()
|
||||
@parentEl.off('mousedown.TicketBatch').on('mousedown.TicketBatch', '.item', @startDragItem)
|
||||
|
||||
renderOptions: =>
|
||||
@renderOptionsGroups()
|
||||
@renderOptionsMacros()
|
||||
|
||||
renderOptionsGroups: =>
|
||||
@batchAssignInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
|
||||
@parent.validUsersForTicketSelection()
|
||||
))
|
||||
|
||||
renderOptionsMacros: =>
|
||||
|
||||
@possibleMacros = []
|
||||
macros = App.Macro.getList()
|
||||
|
||||
items = @parentEl.find('[name="bulk"]:checked')
|
||||
|
||||
group_ids =[]
|
||||
for item in items
|
||||
ticket = App.Ticket.find($(item).val())
|
||||
group_ids.push ticket.group_id
|
||||
|
||||
group_ids = _.uniq(group_ids)
|
||||
|
||||
for macro in macros
|
||||
|
||||
# push if no group_ids exists
|
||||
if _.isEmpty(macro.group_ids) && !_.includes(@possibleMacros, macro)
|
||||
@possibleMacros.push macro
|
||||
|
||||
# push if group_ids are equal
|
||||
if _.isEqual(macro.group_ids, group_ids) && !_.includes(@possibleMacros, macro)
|
||||
@possibleMacros.push macro
|
||||
|
||||
# push if all group_ids of tickets are in macro.group_ids
|
||||
if !_.isEmpty(macro.group_ids) && _.isEmpty(_.difference(group_ids,macro.group_ids)) && !_.includes(@possibleMacros, macro)
|
||||
@possibleMacros.push macro
|
||||
|
||||
@batchMacro.html $(App.view('ticket_overview/batch_overlay_macro')(
|
||||
macros: @possibleMacros
|
||||
))
|
||||
|
||||
startDragItem: (event) =>
|
||||
@grabbedItem = $(event.currentTarget)
|
||||
offset = @grabbedItem.offset()
|
||||
@batchDragger = $(App.view('ticket_overview/batch_dragger')())
|
||||
@grabbedItemClone = @grabbedItem.clone()
|
||||
@grabbedItemClone.data('offset', @grabbedItem.offset())
|
||||
@grabbedItemClone.addClass('batch-dragger-item js-main-item')
|
||||
@batchDragger.append @grabbedItemClone
|
||||
|
||||
@batchDragger.data
|
||||
startX: event.pageX
|
||||
startY: event.pageY
|
||||
dx: Math.min(event.pageX - offset.left, 180)
|
||||
dy: event.pageY - offset.top
|
||||
moved: false
|
||||
|
||||
$(document).on 'mousemove.item', @dragItem
|
||||
$(document).one 'mouseup.item', @endDragItem
|
||||
# TODO: fire @cancelDrag on ESC
|
||||
|
||||
dragItem: (event) =>
|
||||
pos = @batchDragger.data()
|
||||
threshold = 3
|
||||
x = event.pageX - pos.dx
|
||||
y = event.pageY - pos.dy
|
||||
dir = if event.pageY > pos.startY then 1 else -1
|
||||
|
||||
if !pos.moved
|
||||
if Math.abs(event.pageY - pos.startY) > threshold || Math.abs(event.pageX - pos.startX) > threshold
|
||||
@batchDragger.data 'moved', true
|
||||
@el.addClass('u-no-userselect')
|
||||
# check grabbed items batch checkbox to make sure its checked
|
||||
# (could be grabbed without checking the checkbox it)
|
||||
@grabbedItemWasntChecked = !@grabbedItem.find('[name="bulk"]').prop('checked')
|
||||
@grabbedItem.find('[name="bulk"]').prop('checked', true)
|
||||
@grabbedItemClone.find('[name="bulk"]').prop('checked', true)
|
||||
|
||||
additionalItems = @el.find('[name="bulk"]:checked').parents('.item').not(@grabbedItem)
|
||||
additionalItemsClones = additionalItems.clone()
|
||||
@draggedItems = @grabbedItemClone.add(additionalItemsClones)
|
||||
# store offsets for later use
|
||||
additionalItemsClones.each (i, item) -> $(@).data('offset', additionalItems.eq(i).offset())
|
||||
@batchDragger.prepend additionalItemsClones.addClass('batch-dragger-item').get().reverse()
|
||||
if(additionalItemsClones.length)
|
||||
@batchDragger.find('.js-batch-dragger-count').text(@draggedItems.length)
|
||||
|
||||
@renderOptions()
|
||||
|
||||
@appEl.append(@batchDragger)
|
||||
|
||||
@draggedItems.each (i, item) ->
|
||||
dx = $(item).data('offset').left - $(item).offset().left - x
|
||||
dy = $(item).data('offset').top - $(item).offset().top - y
|
||||
$.Velocity.hook item, 'translateX', "#{dx}px"
|
||||
$.Velocity.hook item, 'translateY', "#{dy}px"
|
||||
|
||||
@alignDraggedItems(-dir)
|
||||
|
||||
@mouseY = event.pageY
|
||||
@showBatchOverlay()
|
||||
else
|
||||
return
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
$.Velocity.hook @batchDragger, 'translateX', "#{x}px"
|
||||
$.Velocity.hook @batchDragger, 'translateY', "#{y}px"
|
||||
|
||||
endDragItem: (event) =>
|
||||
$(document).off 'mousemove.item'
|
||||
$(document).off 'mouseup.item'
|
||||
pos = @batchDragger.data()
|
||||
|
||||
@clearDelay('clear-hovered-batch-entry')
|
||||
|
||||
if !@hoveredBatchEntry
|
||||
@cleanUpDrag()
|
||||
return
|
||||
|
||||
$.Velocity.hook @batchDragger, 'transformOriginX', "#{pos.dx}px"
|
||||
$.Velocity.hook @batchDragger, 'transformOriginY', "#{pos.dy}px"
|
||||
@hoveredBatchEntry.velocity
|
||||
properties:
|
||||
scale: 1.1
|
||||
options:
|
||||
duration: 200
|
||||
complete: =>
|
||||
if !@hoveredBatchEntry
|
||||
@cleanUpDrag()
|
||||
return
|
||||
|
||||
@hoveredBatchEntry.velocity 'reverse',
|
||||
duration: 200
|
||||
complete: =>
|
||||
|
||||
if !@hoveredBatchEntry
|
||||
@cleanUpDrag()
|
||||
return
|
||||
|
||||
# clean scale
|
||||
action = @hoveredBatchEntry.attr('data-action')
|
||||
id = @hoveredBatchEntry.attr('data-id')
|
||||
groupId = @hoveredBatchEntry.attr('data-group-id')
|
||||
items = @parentEl.find('[name="bulk"]:checked')
|
||||
@hoveredBatchEntry.removeAttr('style')
|
||||
@cleanUpDrag(true)
|
||||
|
||||
@performBatchAction items, action, id, groupId
|
||||
@batchDragger.velocity
|
||||
properties:
|
||||
scale: 0
|
||||
options:
|
||||
duration: 200
|
||||
|
||||
cancelDrag: ->
|
||||
$(document).off 'mousemove.item'
|
||||
$(document).off 'mouseup.item'
|
||||
@cleanUpDrag()
|
||||
|
||||
cleanUpDrag: (success) ->
|
||||
@hideBatchOverlay()
|
||||
@el.removeClass('u-no-userselect')
|
||||
$('.batch-dragger').remove()
|
||||
@hoveredBatchEntry = null
|
||||
|
||||
if @grabbedItemWasntChecked
|
||||
@grabbedItem.find('[name="bulk"]').prop('checked', false)
|
||||
|
||||
if success
|
||||
# uncheck all checked items
|
||||
@el.find('[name="bulk"]:checked').prop('checked', false)
|
||||
@el.find('[name="bulk_all"]').prop('checked', false)
|
||||
|
||||
alignDraggedItems: (dir) ->
|
||||
@draggedItems.velocity
|
||||
properties:
|
||||
translateX: 0
|
||||
translateY: (i) => dir * i * @batchDragger.height()/2
|
||||
options:
|
||||
easing: 'ease-in-out'
|
||||
duration: 300
|
||||
|
||||
@batchDragger.find('.js-batch-dragger-count').velocity
|
||||
properties:
|
||||
translateY: if dir < 0 then 0 else -@batchDragger.height()+8
|
||||
options:
|
||||
easing: 'ease-in-out'
|
||||
duration: 300
|
||||
|
||||
performBatchAction: (items, action, id, groupId) ->
|
||||
ticket_ids = items.toArray().map (item) -> $(item).val()
|
||||
|
||||
switch action
|
||||
when 'macro'
|
||||
path = 'macro'
|
||||
data =
|
||||
ticket_ids: ticket_ids
|
||||
macro_id: id
|
||||
|
||||
when 'user_assign'
|
||||
path = 'update'
|
||||
|
||||
data =
|
||||
ticket_ids: ticket_ids
|
||||
attributes:
|
||||
owner_id: id
|
||||
|
||||
if !_.isEmpty(groupId)
|
||||
data.attributes.group_id = groupId
|
||||
|
||||
when 'group_assign'
|
||||
path = 'update'
|
||||
|
||||
data =
|
||||
ticket_ids: ticket_ids
|
||||
attributes:
|
||||
group_id: id
|
||||
|
||||
@parent.ajax_mass(path, data, @batchSuccess)
|
||||
|
||||
showBatchOverlay: ->
|
||||
@el.addClass('is-visible')
|
||||
$('html').css('overflow', 'hidden')
|
||||
@batchOverlayBackdrop.velocity { opacity: [1, 0] }, { duration: 500 }
|
||||
@batchMacroOffset = @batchMacro.offset().top + @batchMacro.outerHeight()
|
||||
@batchAssignOffset = @batchAssign.offset().top
|
||||
@batchOverlayShown = true
|
||||
$(document).on 'mousemove.batchoverlay', @controlBatchOverlay
|
||||
|
||||
hideBatchOverlay: ->
|
||||
$(document).off 'mousemove.batchoverlay'
|
||||
@batchOverlayShown = false
|
||||
@batchOverlayBackdrop.velocity { opacity: [0, 1] }, { duration: 300, queue: false }
|
||||
@hideBatchCircles =>
|
||||
@el.removeClass('is-visible')
|
||||
|
||||
$('html').css('overflow', '')
|
||||
|
||||
if @batchAssignShown
|
||||
@hideBatchAssign()
|
||||
|
||||
if @batchMacroShown
|
||||
@hideBatchMacro()
|
||||
|
||||
if @batchAssignGroupShown
|
||||
@hideBatchAssignGroup()
|
||||
|
||||
controlBatchOverlay: (event) =>
|
||||
return if @batchAnimationPaused
|
||||
# store to detect if the mouse is hovering a drag-action entry
|
||||
# after an animation ended -> @highlightBatchEntryAtMousePosition
|
||||
@mouse.x = event.pageX
|
||||
@mouse.y = event.pageY
|
||||
|
||||
if @batchAssignGroupShown && @batchAssignGroupOffset != undefined
|
||||
if @mouse.y < @batchAssignGroupOffset
|
||||
@hideBatchAssignGroup()
|
||||
@batchAnimationPaused = true
|
||||
return
|
||||
|
||||
if @mouse.y <= @batchMacroOffset
|
||||
mouseInArea = 'top'
|
||||
else if @mouse.y > @batchMacroOffset && @mouse.y <= @batchAssignOffset
|
||||
mouseInArea = 'middle'
|
||||
else
|
||||
mouseInArea = 'bottom'
|
||||
|
||||
switch mouseInArea
|
||||
when 'top'
|
||||
if !@batchMacroShown
|
||||
@hideBatchCircles()
|
||||
@showBatchMacro()
|
||||
@alignDraggedItems(1)
|
||||
|
||||
when 'middle'
|
||||
if @batchAssignShown
|
||||
@hideBatchAssign()
|
||||
|
||||
if @batchMacroShown
|
||||
@hideBatchMacro()
|
||||
|
||||
if !@batchCirclesShown
|
||||
@showBatchCircles()
|
||||
|
||||
when 'bottom'
|
||||
if !@batchAssignShown
|
||||
@hideBatchCircles()
|
||||
@showBatchAssign()
|
||||
@alignDraggedItems(-1)
|
||||
|
||||
showBatchCircles: ->
|
||||
@batchCirclesShown = true
|
||||
|
||||
@batchMacroCircle.velocity
|
||||
properties:
|
||||
translateY: [0, '-150%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
delay: 200
|
||||
|
||||
@batchAssignCircle.velocity
|
||||
properties:
|
||||
translateY: [0, '150%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
delay: 200
|
||||
|
||||
hideBatchCircles: (callback) ->
|
||||
@batchMacroCircle.velocity
|
||||
properties:
|
||||
translateY: ['-150%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchAssignCircle.velocity
|
||||
properties:
|
||||
translateY: ['150%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
complete: callback
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchCirclesShown = false
|
||||
|
||||
showBatchAssign: ->
|
||||
return if !@batchOverlayShown # user might have dropped the item already
|
||||
@batchAssignShown = true
|
||||
|
||||
@batchCancel.css
|
||||
top: 0
|
||||
bottom: @batchAssign.height()
|
||||
|
||||
@batchAssign.velocity
|
||||
properties:
|
||||
translateY: [0, '100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
complete: @highlightBatchEntryAtMousePosition
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: [0, '100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
|
||||
hideBatchAssign: ->
|
||||
@batchAssign.velocity
|
||||
properties:
|
||||
translateY: ['100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
complete: =>
|
||||
$.Velocity.hook @batchAssign, 'translateY', '0%'
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: ['100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchAssignShown = false
|
||||
|
||||
showBatchAssignGroup: =>
|
||||
return if !@batchOverlayShown # user might have dropped the item already
|
||||
@batchAssignGroupShown = true
|
||||
|
||||
groupId = @hoveredBatchEntry.attr('data-id')
|
||||
group = App.Group.find(groupId)
|
||||
|
||||
@batchAssignGroupName.text group.displayName()
|
||||
@batchAssignGroupInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
|
||||
users: @parent.usersInGroups([groupId])
|
||||
groups: []
|
||||
groupId: groupId
|
||||
))
|
||||
|
||||
# then adjust the size of the group that it almost overlaps the batch-assign box
|
||||
@batchAssignGroupInner.height(@batchAssignInner.height())
|
||||
|
||||
@batchAssignGroup.velocity
|
||||
properties:
|
||||
translateY: [0, '100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 700
|
||||
visibility: 'visible'
|
||||
complete: =>
|
||||
@highlightBatchEntryAtMousePosition()
|
||||
@batchAssignGroupOffset = @batchAssignGroup.offset().top
|
||||
|
||||
hideBatchAssignGroup: ->
|
||||
@batchAssignGroup.velocity
|
||||
properties:
|
||||
translateY: ['100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
complete: =>
|
||||
@batchAssignGroupShown = false
|
||||
@batchAssignGroupHovered = false
|
||||
setTimeout (=> @batchAnimationPaused = false), 1000
|
||||
|
||||
@batchAssignGroupOffset = undefined
|
||||
|
||||
showBatchMacro: ->
|
||||
return if !@batchOverlayShown # user might have dropped the item already
|
||||
@batchMacroShown = true
|
||||
|
||||
@batchCancel.css
|
||||
bottom: 0
|
||||
top: @batchMacro.height()
|
||||
|
||||
@batchMacro.velocity
|
||||
properties:
|
||||
translateY: [0, '-100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
complete: @highlightBatchEntryAtMousePosition
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: [0, '-100%']
|
||||
opacity: [1, 0]
|
||||
options:
|
||||
easing: [1,-.55,.2,1.37]
|
||||
duration: 500
|
||||
visibility: 'visible'
|
||||
|
||||
hideBatchMacro: ->
|
||||
@batchMacro.velocity
|
||||
properties:
|
||||
translateY: ['-100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
complete: =>
|
||||
$.Velocity.hook @batchMacro, 'translateY', '0%'
|
||||
|
||||
@batchCancel.velocity
|
||||
properties:
|
||||
translateY: ['-100%', 0]
|
||||
opacity: [0, 1]
|
||||
options:
|
||||
duration: 300
|
||||
visibility: 'hidden'
|
||||
queue: false
|
||||
|
||||
@batchMacroShown = false
|
||||
|
||||
highlightBatchEntryAtMousePosition: =>
|
||||
entryAtPoint = $(document.elementFromPoint(@mouse.x, @mouse.y)).closest('.js-batch-overlay-entry .avatar')
|
||||
if(entryAtPoint.length)
|
||||
@hoveredBatchEntry = entryAtPoint.closest('.js-batch-overlay-entry').addClass('is-hovered')
|
||||
|
||||
highlightBatchEntry: (event) =>
|
||||
@clearDelay('clear-hovered-batch-entry')
|
||||
@hoveredBatchEntry = $(event.currentTarget).closest('.js-batch-overlay-entry').addClass('is-hovered')
|
||||
|
||||
if @hoveredBatchEntry.attr('data-action') is 'group_assign'
|
||||
@batchAssignGroupHintTimeout = setTimeout @blinkBatchEntry, 800
|
||||
@batchAssignGroupTimeout = setTimeout @showBatchAssignGroup, 900
|
||||
|
||||
unhighlightBatchEntry: (event) ->
|
||||
return if !@hoveredBatchEntry
|
||||
if @hoveredBatchEntry.attr('data-action') is 'group_assign'
|
||||
if @batchAssignGroupTimeout
|
||||
clearTimeout @batchAssignGroupTimeout
|
||||
if @batchAssignGroupHintTimeout
|
||||
clearTimeout @batchAssignGroupHintTimeout
|
||||
|
||||
@hoveredBatchEntry.removeClass('is-hovered')
|
||||
delay = =>
|
||||
@hoveredBatchEntry = null
|
||||
@delay(delay, 800, 'clear-hovered-batch-entry')
|
||||
|
||||
blinkBatchEntry: =>
|
||||
@hoveredBatchEntry
|
||||
.velocity({ opacity: [0.5, 1] }, { duration: 120 })
|
||||
.velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
|
||||
.velocity({ opacity: [0.5, 1] }, { duration: 120 })
|
||||
.velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
|
||||
@@ -240,7 +240,7 @@ class App.TicketBulkForm extends App.Controller
|
||||
|
||||
@ajax_mass_update(data, =>
|
||||
@holder.find('.table').find('[name="bulk"]:checked').prop('checked', false)
|
||||
@render()
|
||||
@batchSuccess()
|
||||
@hide()
|
||||
)
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class App.WidgetUser extends App.Controller
|
||||
elLocal.find('.js-organizationList').prepend(organizations)
|
||||
)
|
||||
|
||||
if @user.organization_ids.length < @organizationLimit
|
||||
if @user.organization_ids && @user.organization_ids.length < @organizationLimit
|
||||
@el.find('.js-showMoreOrganizations').addClass('hidden')
|
||||
else
|
||||
@el.find('.js-showMoreOrganizations').removeClass('hidden')
|
||||
|
||||
@@ -85,7 +85,7 @@ class App.GlobalSearch extends App.Controller
|
||||
params.callbackMatch()
|
||||
|
||||
# if result hasn't changed, do not rerender
|
||||
if @lastQuery is query && @searchResultCache[query]
|
||||
if !params.force && @lastQuery is query && @searchResultCache[query]
|
||||
diff = difference(@searchResultCache[query].result, result)
|
||||
if _.isEmpty(diff)
|
||||
return
|
||||
|
||||
@@ -459,12 +459,12 @@ class App.SearchableSelect extends Spine.Controller
|
||||
content = {}
|
||||
if @attribute.relation
|
||||
content =
|
||||
name: name
|
||||
name: String(name)
|
||||
value: value
|
||||
object: @attribute.relation
|
||||
else
|
||||
content =
|
||||
name: value
|
||||
name: String(value)
|
||||
value: value
|
||||
|
||||
@input.before App.view('generic/token')(content)
|
||||
|
||||
@@ -224,7 +224,16 @@ class App.Utils
|
||||
|
||||
# htmlEscapedAndLinkified = App.Utils.linkify(rawText)
|
||||
@linkify: (string) ->
|
||||
window.linkify(string)
|
||||
window.linkifyStr(string, {
|
||||
attributes: @_getLinkifyAttributes,
|
||||
})
|
||||
|
||||
@_getLinkifyAttributes: (href) ->
|
||||
attributes = {
|
||||
title: href,
|
||||
}
|
||||
if /^https?:/.test(href) then attributes.target = '_blank'
|
||||
return attributes
|
||||
|
||||
# htmlEscapedAndPhoneified = App.Utils.phoneify(rawText)
|
||||
@phoneify: (string) ->
|
||||
@@ -780,6 +789,26 @@ class App.Utils
|
||||
|
||||
@signatureIdentifyByHtmlHelper(message)
|
||||
|
||||
@signatureRemoveByHtml: (message) ->
|
||||
container = document.createElement('container-element')
|
||||
container.innerHTML = message
|
||||
|
||||
brsToRemove = []
|
||||
|
||||
signatures = container
|
||||
.querySelectorAll('div[data-signature]')
|
||||
.forEach (elem) ->
|
||||
node = elem
|
||||
while(node?.previousSibling?.nodeName == 'BR')
|
||||
brsToRemove.push node.previousSibling
|
||||
node = node.previousSibling
|
||||
|
||||
elem.remove()
|
||||
|
||||
brsToRemove.forEach (elem) -> elem.remove()
|
||||
|
||||
container.innerHTML
|
||||
|
||||
@signatureIdentifyByHtmlHelper: (message, internal = false) ->
|
||||
# blockquotes and signature blocks are considered "dismiss nodes" and their indice will be stored
|
||||
dismissNodes = []
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/*!
|
||||
* linkify - v0.3 - 6/27/2009
|
||||
* http://benalman.com/code/test/js-linkify/
|
||||
*
|
||||
* Copyright (c) 2009 "Cowboy" Ben Alman
|
||||
* Licensed under the MIT license
|
||||
* http://benalman.com/about/license/
|
||||
*
|
||||
* Some regexps adapted from http://userscripts.org/scripts/review/7122
|
||||
*/
|
||||
|
||||
// Turn text into linkified html.
|
||||
//
|
||||
// var html = linkify( text, options );
|
||||
//
|
||||
// options:
|
||||
//
|
||||
// callback (Function) - default: undefined - if defined, this will be called
|
||||
// for each link- or non-link-chunk with two arguments, text and href. If the
|
||||
// chunk is non-link, href will be omitted.
|
||||
//
|
||||
// punct_regexp (RegExp | Boolean) - a RegExp that can be used to trim trailing
|
||||
// punctuation from links, instead of the default.
|
||||
//
|
||||
// This is a work in progress, please let me know if (and how) it fails!
|
||||
|
||||
window.linkify = (function(){
|
||||
var
|
||||
SCHEME = "[a-z\\d.-]+://",
|
||||
IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",
|
||||
HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",
|
||||
TLD = "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)",
|
||||
HOST_OR_IP = "(?:" + HOSTNAME + TLD + "|" + IPV4 + ")",
|
||||
PATH = "(?:[;/][^#?<>\\s]*)?",
|
||||
QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",
|
||||
URI1 = "\\b" + SCHEME + "[^<>\\s]+",
|
||||
URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)",
|
||||
|
||||
MAILTO = "mailto:",
|
||||
EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)",
|
||||
|
||||
URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ),
|
||||
SCHEME_RE = new RegExp( "^" + SCHEME, "i" ),
|
||||
|
||||
quotes = {
|
||||
"'": "`",
|
||||
'>': '<',
|
||||
')': '(',
|
||||
']': '[',
|
||||
'}': '{',
|
||||
'»': '«',
|
||||
'›': '‹'
|
||||
},
|
||||
|
||||
default_options = {
|
||||
callback: function( text, href ) {
|
||||
// return href ? '<a href="' + href + '" title="' + href + '">' + text + '<\/a>' : text;
|
||||
return href ? '<a href="' + href + '" title="' + href + '" target="_blank">' + text + '<\/a>' : text;
|
||||
},
|
||||
punct_regexp: /(?:[!?.,:;'"]|(?:&|&)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/
|
||||
};
|
||||
|
||||
return function( txt, options ) {
|
||||
options = options || {};
|
||||
|
||||
// me
|
||||
txt = txt
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
// me
|
||||
// Temp variables.
|
||||
var arr,
|
||||
i,
|
||||
link,
|
||||
href,
|
||||
|
||||
// Output HTML.
|
||||
html = '',
|
||||
|
||||
// Store text / link parts, in order, for re-combination.
|
||||
parts = [],
|
||||
|
||||
// Used for keeping track of indices in the text.
|
||||
idx_prev,
|
||||
idx_last,
|
||||
idx,
|
||||
link_last,
|
||||
|
||||
// Used for trimming trailing punctuation and quotes from links.
|
||||
matches_begin,
|
||||
matches_end,
|
||||
quote_begin,
|
||||
quote_end;
|
||||
|
||||
// Initialize options.
|
||||
for ( i in default_options ) {
|
||||
if ( options[ i ] === undefined ) {
|
||||
options[ i ] = default_options[ i ];
|
||||
}
|
||||
}
|
||||
|
||||
// Find links.
|
||||
while ( arr = URI_RE.exec( txt ) ) {
|
||||
|
||||
link = arr[0];
|
||||
idx_last = URI_RE.lastIndex;
|
||||
idx = idx_last - link.length;
|
||||
|
||||
// Not a link if preceded by certain characters.
|
||||
if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trim trailing punctuation.
|
||||
do {
|
||||
// If no changes are made, we don't want to loop forever!
|
||||
link_last = link;
|
||||
|
||||
quote_end = link.substr( -1 )
|
||||
quote_begin = quotes[ quote_end ];
|
||||
|
||||
// Ending quote character?
|
||||
if ( quote_begin ) {
|
||||
matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) );
|
||||
matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) );
|
||||
|
||||
// If quotes are unbalanced, remove trailing quote character.
|
||||
if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) {
|
||||
link = link.substr( 0, link.length - 1 );
|
||||
idx_last--;
|
||||
}
|
||||
}
|
||||
|
||||
// Ending non-quote punctuation character?
|
||||
if ( options.punct_regexp ) {
|
||||
link = link.replace( options.punct_regexp, function(a){
|
||||
idx_last -= a.length;
|
||||
return '';
|
||||
});
|
||||
}
|
||||
} while ( link.length && link !== link_last );
|
||||
|
||||
href = link;
|
||||
|
||||
// Add appropriate protocol to naked links.
|
||||
if ( !SCHEME_RE.test( href ) ) {
|
||||
href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO )
|
||||
: !href.indexOf( 'irc.' ) ? 'irc://'
|
||||
: !href.indexOf( 'ftp.' ) ? 'ftp://'
|
||||
: 'http://' )
|
||||
+ href;
|
||||
}
|
||||
|
||||
// Push preceding non-link text onto the array.
|
||||
if ( idx_prev != idx ) {
|
||||
parts.push([ txt.slice( idx_prev, idx ) ]);
|
||||
idx_prev = idx_last;
|
||||
}
|
||||
|
||||
// Push massaged link onto the array
|
||||
// 2018-10-30: me only link urls, not mailto link
|
||||
//parts.push([ link, href ]);
|
||||
if ( href && href.substr && href.substr(0,7) != 'mailto:') {
|
||||
parts.push([ link, href ]);
|
||||
}
|
||||
else {
|
||||
parts.push([ link, undefined ]);
|
||||
}
|
||||
};
|
||||
|
||||
// Push remaining non-link text onto the array.
|
||||
parts.push([ txt.substr( idx_prev ) ]);
|
||||
|
||||
// Process the array items.
|
||||
for ( i = 0; i < parts.length; i++ ) {
|
||||
html += options.callback.apply( window, parts[i] );
|
||||
}
|
||||
|
||||
// In case of catastrophic failure, return the original text;
|
||||
return html || txt;
|
||||
};
|
||||
|
||||
})();
|
||||
@@ -490,7 +490,7 @@
|
||||
|
||||
// Clear `cropBox` is necessary when replace
|
||||
this.cropBox = null;
|
||||
this.off();
|
||||
this.unbind();
|
||||
|
||||
this.resetPreview();
|
||||
this.$preview = null;
|
||||
|
||||
@@ -518,12 +518,30 @@
|
||||
callback(item.content, [])
|
||||
}
|
||||
|
||||
function matchItemWithRegExp(item, regex) {
|
||||
return (item.name && item.name.match(regex)) || (item.keywords && item.keywords.match(regex))
|
||||
}
|
||||
|
||||
function compareItem(a, b) {
|
||||
if (a.name < b.name) return 1;
|
||||
if (a.name > b.name) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
Collection.renderResults = function(textmodule, term) {
|
||||
var reg = new RegExp(term, 'i')
|
||||
var result = textmodule.collection.filter(function(item) {
|
||||
return (item.name && item.name.match(reg)) || (item.keywords && item.keywords.match(reg))
|
||||
var reg = new RegExp(term, 'i')
|
||||
var regFull = new RegExp('\\b' + term + '\\b', 'i')
|
||||
var result = textmodule.collection.filter(function(item) {
|
||||
return matchItemWithRegExp(item, reg) && !matchItemWithRegExp(item, regFull)
|
||||
})
|
||||
result.reverse()
|
||||
var resultFull = textmodule.collection.filter(function(item) {
|
||||
return matchItemWithRegExp(item, regFull)
|
||||
})
|
||||
result.sort(compareItem)
|
||||
if (resultFull.length) {
|
||||
resultFull.sort(compareItem)
|
||||
result = result.concat(resultFull)
|
||||
}
|
||||
|
||||
textmodule.emptyResultsContainer()
|
||||
|
||||
@@ -578,10 +596,8 @@
|
||||
}),
|
||||
url: translation.parent().generateURL('/attachments/clone_to_form'),
|
||||
success: function(data, status, xhr) {
|
||||
translation.parent().attachments += data.attachments
|
||||
|
||||
App.Utils.htmlImage2DataUrlAsync(body, function(output){
|
||||
callback(output, translation.parent().attachments)
|
||||
callback(output, data.attachments)
|
||||
})
|
||||
},
|
||||
error: function(xhr) {
|
||||
|
||||
3092
app/assets/javascripts/app/lib/base/linkify.js
vendored
Normal file
3092
app/assets/javascripts/app/lib/base/linkify.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,17 @@
|
||||
App.PermissionHelper =
|
||||
switchGroupPermission: (event) ->
|
||||
throttled = _.throttle( (e) ->
|
||||
input = $(e.target)
|
||||
|
||||
upcoming_state = input.prop('checked')
|
||||
if !$(e.target).is(':checkbox')
|
||||
upcoming_state = !upcoming_state
|
||||
|
||||
selector = 'input[value=full]'
|
||||
if input.val() is 'full' and upcoming_state is true
|
||||
selector = 'input[value!=full]'
|
||||
|
||||
$(e.target).closest('tr').find(selector).prop('checked', false)
|
||||
, 300, { trailing: false })
|
||||
|
||||
throttled(event)
|
||||
@@ -1,6 +1,6 @@
|
||||
App.ValidUsersForTicketSelectionMethods =
|
||||
validUsersForTicketSelection: ->
|
||||
items = $('.content.active .table-overview .table').find('[name="bulk"]:checked')
|
||||
items = $('.content.active .main .table').find('[name="bulk"]:checked')
|
||||
|
||||
# we want to display all users for which we can assign the tickets directly
|
||||
# for this we need to get the groups of all selected tickets
|
||||
@@ -17,7 +17,6 @@ App.ValidUsersForTicketSelectionMethods =
|
||||
group_ids = _.keys(@formMeta?.dependencies?.group_id)
|
||||
groups = App.Group.findAll(group_ids)
|
||||
groups_sorted = _.sortBy(groups, (group) -> group.name)
|
||||
|
||||
# get the number of visible users per group
|
||||
# from the TicketOverviewCollection
|
||||
# (filled for e.g. the TicketCreation or TicketZoom assignment)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class App.Permission extends App.Model
|
||||
@configure 'Permission', 'name', 'note', 'active'
|
||||
@configure 'Permission', 'name', 'note', 'active', 'preferences'
|
||||
@extend Spine.Model.Ajax
|
||||
@url: @apiPath + '/permissions'
|
||||
|
||||
@@ -7,7 +7,7 @@ class App.PostmasterFilter extends App.Model
|
||||
{ name: 'name', display: __('Name'), tag: 'input', type: 'text', limit: 250, 'null': false },
|
||||
{ name: 'channel', display: __('Channel'), type: 'input', readonly: 1 },
|
||||
{ name: 'match', display: __('Match all of the following'), tag: 'postmaster_match', note: __('You can use regular expression by using "regex:your_reg_exp".') },
|
||||
{ name: 'perform', display: __('Perform action of the following'), tag: 'postmaster_set' },
|
||||
{ name: 'perform', display: __('Perform action of the following'), tag: 'postmaster_set', user_action: false },
|
||||
{ name: 'note', display: __('Note'), tag: 'textarea', limit: 250, null: true },
|
||||
{ name: 'updated_at', display: __('Updated'), tag: 'datetime', readonly: 1 },
|
||||
{ name: 'active', display: __('Active'), tag: 'active', default: true },
|
||||
@@ -23,4 +23,4 @@ class App.PostmasterFilter extends App.Model
|
||||
@configure_clone = true
|
||||
|
||||
@on 'create', (newRecord) ->
|
||||
newRecord.channel = 'email'
|
||||
newRecord.channel = 'email'
|
||||
|
||||
@@ -258,8 +258,10 @@ class App.Ticket extends App.Model
|
||||
|
||||
@_selectorMatch: (object, objectName, attributeName, condition) ->
|
||||
conditionValue = condition.value
|
||||
conditionValue = '' if conditionValue == null
|
||||
conditionValue = '' if conditionValue == undefined
|
||||
objectValue = object[attributeName]
|
||||
objectValue = '' if objectValue == null
|
||||
objectValue = '' if objectValue == undefined
|
||||
|
||||
# take care about pre conditions
|
||||
|
||||
@@ -378,8 +378,11 @@ class App.User extends App.Model
|
||||
_.uniq(result)
|
||||
|
||||
secondaryOrganizations: (offset, limit, callback) ->
|
||||
organization_ids = @organization_ids.slice(offset, limit)
|
||||
missing_organization_ids = _.filter(organization_ids, (id) -> !App.Organization.findNative(id))
|
||||
organization_ids = []
|
||||
missing_organization_ids = []
|
||||
if _.isArray(@organization_ids)
|
||||
organization_ids = @organization_ids.slice(offset, limit)
|
||||
missing_organization_ids = _.filter(organization_ids, (id) -> !App.Organization.findNative(id))
|
||||
|
||||
organizationResult = ->
|
||||
organizations = []
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
<% end %>
|
||||
|
||||
<% if _.isEmpty(@account_channels): %>
|
||||
<p><%- @T('You have no configured account right now.') %></p>
|
||||
<table class="table table--placeholder">
|
||||
<thead><tr><th><%- @T('No Entries') %>
|
||||
</table>
|
||||
<% else: %>
|
||||
<% for channel in @account_channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%- channel.id %>">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<h2><%- @T('SMS Accounts') %></h2>
|
||||
|
||||
<% if _.isEmpty(@account_channels): %>
|
||||
<p><%- @T('You have no configured account right now.') %></p>
|
||||
<table class="table table--placeholder">
|
||||
<thead><tr><th><%- @T('No Entries') %>
|
||||
</table>
|
||||
<% else: %>
|
||||
<% for channel in @account_channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%- channel.id %>">
|
||||
@@ -85,7 +87,9 @@
|
||||
|
||||
<h2><%- @T('SMS Notification') %></h2>
|
||||
<% if _.isEmpty(@notification_channels): %>
|
||||
<p><%- @T('You have no configured account right now.') %></p>
|
||||
<table class="table table--placeholder">
|
||||
<thead><tr><th><%- @T('No Entries') %>
|
||||
</table>
|
||||
<a class="btn btn--success js-editNotification"><%- @T('New') %></a>
|
||||
<% else: %>
|
||||
<% for channel in @notification_channels: %>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<% if @item.objectNative && @item.objectNative.activityMessage: %>
|
||||
<%- @item.objectNative.activityMessage(@item) %>
|
||||
<% else: %>
|
||||
Need objectNative in item <%= item.object %>.find(<%= item.o_id %>)
|
||||
Need objectNative in item <%= @item.object %>.find(<%= @item.o_id %>)
|
||||
<% end %>
|
||||
</span>
|
||||
<%- @humanTime(@item.created_at, false, 'activity-time') %>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</h2>
|
||||
<p class="help-text"><% if @attribute.help: %><%- @T(@attribute.help) %><% end %><%- @attribute.helpLink %></p>
|
||||
<% end %>
|
||||
<div class="controls <% if ( @attribute.type is 'url' || !_.isEmpty(@attribute.linktemplate) ) && !_.isEmpty(@placeholderObjects): %>controls-button<% end %>">
|
||||
<div class="controls <% if ( @attribute.type is 'url' || !_.isEmpty(@attribute.linktemplate) ) && !_.isEmpty(@placeholderObjects): %>controls-button ignore-readonly<% end %>">
|
||||
<% if ( @attribute.type is 'url' || !_.isEmpty(@attribute.linktemplate) ) && !_.isEmpty(@placeholderObjects): %>
|
||||
<a href="<% if @attribute.type is 'url': %><%= @placeholderObjects[@className.toLowerCase()][@attribute.name] %><% else: %><%- @replacePlaceholder(@attribute.linktemplate, @placeholderObjects, true) %><% end %>" class="controls-button" target="_blank" rel="nofollow">
|
||||
<span class="controls-button-inner"><%- @Icon('external') %></span>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<%= @attribute.autofocus %>
|
||||
value="<%= @attribute.value %>"
|
||||
multiple
|
||||
disabled
|
||||
>
|
||||
<% for option in @allOptions: %>
|
||||
<option value="<%= option.value %>" <%= ' selected' if option.selected %>><%= option.name %></option>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
value
|
||||
name="<%- @attribute.name %>_completion"
|
||||
autocomplete="off"
|
||||
<%= @attribute.required %>
|
||||
<% if @attribute.disabled: %> disabled<% end %>
|
||||
>
|
||||
<% else: %>
|
||||
@@ -22,7 +21,6 @@
|
||||
class="searchableSelect-shadow form-control js-shadow"
|
||||
<% if @attribute.id: %>id="<%= @attribute.id %>"<% end %>
|
||||
name="<%= @attribute.name %>"
|
||||
<%= @attribute.required %>
|
||||
<%= @attribute.autofocus %>
|
||||
value="<%= @attribute.value %>"
|
||||
tabindex="-1"
|
||||
@@ -33,7 +31,6 @@
|
||||
placeholder="<%= @attribute.placeholder %>"
|
||||
value="<%= @attribute.valueName %>"
|
||||
autocomplete="off"
|
||||
<%= @attribute.required %>
|
||||
<% if @attribute.disabled: %> disabled<% end %>
|
||||
>
|
||||
<% end %>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</a>
|
||||
<%- @humanTime(ticket.created_at, false, 'time') %>
|
||||
</div>
|
||||
<% if @object: %>
|
||||
<% if @object && @editable: %>
|
||||
<a class="list-item-delete js-delete" data-object="<%= @object %>" data-object-id="<%= ticket.id %>" data-link-type="<%= @linkType || 'normal' %>" data-type="remove">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</a>
|
||||
@@ -31,4 +31,4 @@
|
||||
<div data-show="collapsed"><%- @Icon('arrow-up') %> <span><%- @T('Show less') %></span></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="flex horizontal">
|
||||
<div class="controls half-space-right">
|
||||
<div class="controls half-space-right flex-no-shrink">
|
||||
<div class="u-positionOrigin js-operator">
|
||||
<select></select>
|
||||
<%- @Icon('arrow-down', 'dropdown-arrow') %>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<%- @Icon('arrow-down', 'dropdown-arrow') %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls js-setAttribute horizontal"></div>
|
||||
<div class="controls js-setAttribute horizontal span-width"></div>
|
||||
<div class="horizontal-filter-group js-setNotification flex"></div>
|
||||
<div class="horizontal-filter-group js-setArticle flex"></div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<select class="form-control js-value" name="<%= @attribute.name %>::value">
|
||||
<% for key, value of @values: %>
|
||||
<option value="<%= key %>" <% if @attribute.value && @attribute.value.value.toString() is key.toString(): %>selected<% end %>><%- value %></option>
|
||||
<option value="<%= key %>" <% if @selected_value && @selected_value.toString() is key.toString(): %>selected<% end %>><%- value %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<%- @Icon('arrow-down', 'dropdown-arrow') %>
|
||||
<%- @Icon('arrow-down', 'dropdown-arrow') %>
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
<% end %>
|
||||
|
||||
<div class="page-content">
|
||||
<% if _.isEmpty(@channels): %>
|
||||
<table class="table table--placeholder">
|
||||
<thead><tr><th><%- @T('No Entries') %>
|
||||
</table>
|
||||
<% end %>
|
||||
<% for channel in @channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
|
||||
<div class="action-flow" style="width: 100%;">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2><%- @T('Linked Tickets') %></h2>
|
||||
</div>
|
||||
|
||||
<%- @V('generic/ticket_list', show_max: 5, tickets: @tickets, object: 'Ticket') %>
|
||||
<%- @V('generic/ticket_list', show_max: 5, tickets: @tickets, object: 'Ticket', editable: @editable) %>
|
||||
|
||||
<a class="btn btn--action btn--small js-add" href="#">
|
||||
<%- @Icon('plus-small') %> <span><%- @T('Add') %></span>
|
||||
|
||||
@@ -8,9 +8,13 @@
|
||||
<div class="task-text">
|
||||
<a class="name kb-answer-popover" data-id="<%- item.id %>" href="<%- item.url %>"><%= item.title %></a>
|
||||
</div>
|
||||
<a class="list-item-delete js-delete" data-object="KnowledgeBase::Answer::Translation" data-object-id="<%= item.id %>" data-link-type="normal">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</a>
|
||||
<% if @editable: %>
|
||||
<a class="list-item-delete js-delete" data-object="KnowledgeBase::Answer::Translation" data-object-id="<%= item.id %>" data-link-type="normal">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</a>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ol>
|
||||
<div class="text-muted js-add u-clickable">+ <%- @T('Link Related Answer') %></div>
|
||||
<% if @editable: %>
|
||||
<div class="text-muted js-add u-clickable">+ <%- @T('Link Related Answer') %></div>
|
||||
<% end %>
|
||||
|
||||
@@ -5,4 +5,6 @@
|
||||
<%- @links[type].ticketList %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="text-muted u-clickable js-add">+ <%- @T('Add Link') %></div>
|
||||
<% if @editable: %>
|
||||
<div class="text-muted u-clickable js-add">+ <%- @T('Add Link') %></div>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>
|
||||
<%- @T('The admin consent request was sent to your administrator.') %><br>
|
||||
<%- @T('Please wait until you received the notification email of the approval.') %><br><br>
|
||||
<%- @T('After approval, please re-connect your Microsoft365 mailbox.') %><br>
|
||||
</p>
|
||||
@@ -6,6 +6,7 @@
|
||||
<div class="page-header-meta">
|
||||
<a class="btn js-configApp"><%- @T('Configure App') %></a>
|
||||
<a class="btn btn--success js-new"><%- @T('Add Account') %></a>
|
||||
<a class="btn btn--success js-admin-consent"><%- @T('Request Admin Consent') %></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,6 +32,11 @@
|
||||
<% end %>
|
||||
|
||||
<div class="page-content">
|
||||
<% if _.isEmpty(@channels): %>
|
||||
<table class="table table--placeholder">
|
||||
<thead><tr><th><%- @T('No Entries') %>
|
||||
</table>
|
||||
<% end %>
|
||||
<% for channel in @channels: %>
|
||||
<div class="action <% if channel.active isnt true: %>is-inactive<% end %>" data-id="<%= channel.id %>">
|
||||
<div class="action-flow" style="width: 100%;">
|
||||
|
||||
@@ -11,8 +11,19 @@
|
||||
<p>
|
||||
<%- @T('After installing, updating, or uninstalling packages the following commands need to be executed on the server:') %>
|
||||
<ul>
|
||||
<% for command in @commands: %>
|
||||
<li><code>zammad> <%- command %></code></li>
|
||||
<% if @package_installation: %>
|
||||
<% if @local_gemfiles: %>
|
||||
<li><code>root> zammad config:set BUNDLE_DEPLOYMENT=0</code></li>
|
||||
<li><code>root> zammad run bundle install --no-deployment</code></li>
|
||||
<% end %>
|
||||
<li><code>root> zammad run rake zammad:package:migrate</code></strong></li>
|
||||
<li><code>root> zammad run rake assets:precompile</code></strong></li>
|
||||
<% else: %>
|
||||
<% if @local_gemfiles: %>
|
||||
<li><code>zammad> bundle install</code></li>
|
||||
<% end %>
|
||||
<li><code>zammad> rake zammad:package:migrate</code></strong></li>
|
||||
<li><code>zammad> rake assets:precompile</code></strong></li>
|
||||
<% end %>
|
||||
<li><code>root> systemctl restart zammad</code></li>
|
||||
</ul>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user