Merge branch 'develop' into private-pull-request-1118

This commit is contained in:
Rolf Schmidt 2017-09-07 14:33:51 +02:00
commit 6ab397ef44
573 changed files with 25280 additions and 5097 deletions

View file

@ -3,7 +3,7 @@ before_script:
- which ruby - which ruby
- env - env
- test -n "$RNAME" && script/build/test_db_config.sh - test -n "$RNAME" && script/build/test_db_config.sh
- test -n "$RNAME" && bundle install - test -n "$RNAME" && bundle install --jobs 8
stages: stages:
- pre - pre
@ -127,6 +127,17 @@ test:integration:email_deliver:
- ruby -I test/ test/integration/email_deliver_test.rb - ruby -I test/ test/integration/email_deliver_test.rb
- rake db:drop - rake db:drop
test:integration:email_keep_on_server:
stage: test
tags:
- core
script:
- export RAILS_ENV=test
- rake db:create
- rake db:migrate
- ruby -I test/ test/integration/email_keep_on_server_test.rb
- rake db:drop
test:integration:twitter: test:integration:twitter:
stage: test stage: test
tags: tags:
@ -229,7 +240,7 @@ test:integration:slack:
- rake db:create - rake db:create
- rake db:migrate - rake db:migrate
- echo "gem 'slack-api'" >> Gemfile.local - echo "gem 'slack-api'" >> Gemfile.local
- bundle install - bundle install --jobs 8
- ruby -I test test/integration/slack_test.rb - ruby -I test test/integration/slack_test.rb
- rake db:drop - rake db:drop
@ -281,6 +292,7 @@ test:integration:es_mysql:
- ruby -I test/ test/integration/elasticsearch_test.rb - ruby -I test/ test/integration/elasticsearch_test.rb
- ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- rake db:drop - rake db:drop
test:integration:es_postgresql: test:integration:es_postgresql:
@ -297,6 +309,7 @@ test:integration:es_postgresql:
- ruby -I test/ test/integration/elasticsearch_test.rb - ruby -I test/ test/integration/elasticsearch_test.rb
- ruby -I test/ test/controllers/search_controller_test.rb - ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- rake db:drop - rake db:drop
test:integration:zendesk_mysql: test:integration:zendesk_mysql:
@ -330,7 +343,7 @@ test:integration:otrs_5_mysql:
- mysql - mysql
script: script:
- export RAILS_ENV=test - export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz599.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"
- rake db:create - rake db:create
- rake db:migrate - rake db:migrate
- ruby -I test/ test/integration/otrs_import_test.rb - ruby -I test/ test/integration/otrs_import_test.rb
@ -343,7 +356,7 @@ test:integration:otrs_5_postgresql:
- postgresql - postgresql
script: script:
- export RAILS_ENV=test - export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz599.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"
- rake db:create - rake db:create
- rake db:migrate - rake db:migrate
- ruby -I test/ test/integration/otrs_import_test.rb - ruby -I test/ test/integration/otrs_import_test.rb
@ -409,6 +422,7 @@ browser:build:
- rake assets:precompile - rake assets:precompile
- rake db:drop - rake db:drop
artifacts: artifacts:
expire_in: 1 week
paths: paths:
- public/assets/.sprockets-manifest* - public/assets/.sprockets-manifest*
- public/assets/application-* - public/assets/application-*
@ -426,7 +440,7 @@ test:browser:integration:api_client_ruby:
- script/build/test_startup.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 - script/build/test_startup.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1
- git clone git@github.com:zammad/zammad-api-client-ruby.git || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1 - git clone git@github.com:zammad/zammad-api-client-ruby.git || script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1
- cd zammad-api-client-ruby - cd zammad-api-client-ruby
- bundle install - bundle install --jobs 8
- export TEST_URL=http://$IP:$BROWSER_PORT - export TEST_URL=http://$IP:$BROWSER_PORT
- rspec || (cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1) - rspec || (cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 1 1)
- cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 0 1 - cd .. && script/build/test_shutdown.sh $RAILS_ENV $BROWSER_PORT $WS_PORT 0 1

View file

@ -5,19 +5,23 @@ notifications: false
targets: targets:
centos-7: centos-7:
dependencies: dependencies:
- elasticsearch
- nginx - nginx
- postgresql-server - postgresql-server
- which - which
debian-8: debian-8:
dependencies: dependencies:
- elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite - postgresql|mysql-server|mariadb-server|sqlite
ubuntu-16.04: ubuntu-16.04:
dependencies: dependencies:
- elasticsearch
- nginx|apache2 - nginx|apache2
- postgresql|mysql-server|mariadb-server|sqlite - postgresql|mysql-server|mariadb-server|sqlite
sles-12: sles-12:
dependencies: dependencies:
- elasticsearch
- nginx - nginx
- postgresql-server - postgresql-server
before: before:

View file

@ -1 +1 @@
2.3.1 2.4.1

View file

@ -19,7 +19,7 @@ services:
- mysql - mysql
language: ruby language: ruby
rvm: rvm:
- 2.3.1 - 2.4.1
before_install: before_install:
- sudo apt-get -qq update - sudo apt-get -qq update
- sudo apt-get install -y curl git-core patch build-essential bison zlib1g-dev libssl-dev libxml2-dev libxml2-dev sqlite3 libsqlite3-dev autotools-dev libxslt1-dev libyaml-0-2 autoconf automake libreadline6-dev libyaml-dev libtool libgmp-dev libgdbm-dev libncurses5-dev pkg-config libffi-dev libmysqlclient-dev postfix - sudo apt-get install -y curl git-core patch build-essential bison zlib1g-dev libssl-dev libxml2-dev libxml2-dev sqlite3 libsqlite3-dev autotools-dev libxslt1-dev libyaml-0-2 autoconf automake libreadline6-dev libyaml-dev libtool libgmp-dev libgdbm-dev libncurses5-dev pkg-config libffi-dev libmysqlclient-dev postfix
@ -55,3 +55,4 @@ script:
- ruby -I test/ test/integration/user_device_controller_test.rb - ruby -I test/ test/integration/user_device_controller_test.rb
- ruby -I test/ test/integration/sipgate_controller_test.rb - ruby -I test/ test/integration/sipgate_controller_test.rb
- rake db:drop - rake db:drop
after_success: contrib/travis-ci.org/trigger-docker-compose-build.sh

View file

@ -1,7 +1,7 @@
# Change Log # Change Log
## [1.6.0](https://github.com/zammad/zammad/tree/1.6.0) (2017-xx-xx) ## [2.1.0](https://github.com/zammad/zammad/tree/2.1.0) (2017-xx-xx)
[Full Changelog](https://github.com/zammad/zammad/compare/1.4.0...1.5.0) [Full Changelog](https://github.com/zammad/zammad/compare/2.0.0...2.1.0)
**Implemented enhancements:** **Implemented enhancements:**

12
Gemfile
View file

@ -1,8 +1,8 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '2.3.1' ruby '2.4.1'
gem 'rails', '4.2.7.1' gem 'rails', '4.2.9'
gem 'rails-observers' gem 'rails-observers'
gem 'activerecord-session_store' gem 'activerecord-session_store'
@ -40,18 +40,20 @@ gem 'omniauth-gitlab'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
gem 'omniauth-linkedin-oauth2' gem 'omniauth-linkedin-oauth2'
gem 'omniauth-twitter' gem 'omniauth-twitter'
gem 'omniauth-microsoft-office365'
gem 'twitter' gem 'twitter'
gem 'telegramAPI' gem 'telegramAPI'
gem 'koala' gem 'koala'
gem 'mail' gem 'mail'
gem 'email_verifier' gem 'valid_email2'
gem 'htmlentities' gem 'htmlentities'
gem 'mime-types' gem 'mime-types'
gem 'biz' gem 'biz'
gem 'composite_primary_keys'
gem 'delayed_job_active_record' gem 'delayed_job_active_record'
gem 'daemons' gem 'daemons'
@ -72,12 +74,16 @@ gem 'argon2'
gem 'writeexcel' gem 'writeexcel'
gem 'icalendar' gem 'icalendar'
gem 'icalendar-recurrence'
gem 'browser' gem 'browser'
# integrations # integrations
gem 'slack-notifier' gem 'slack-notifier'
gem 'clearbit' gem 'clearbit'
gem 'zendesk_api' gem 'zendesk_api'
gem 'viewpoint'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git'
# event machine # event machine
gem 'eventmachine' gem 'eventmachine'

View file

@ -1,67 +1,82 @@
GIT
remote: https://github.com/thorsteneckel/autodiscover.git
revision: 29d713ee0c8c25fcf74c4292ff13fe1fa4d0d827
specs:
autodiscover (1.0.2)
httpclient
logging
nokogiri
nori
GIT
remote: https://github.com/wimm/rubyntlm.git
revision: 53969639b87b9e5d5fef560f19cf0d977259591c
specs:
rubyntlm (0.1.2)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actionmailer (4.2.7.1) actionmailer (4.2.9)
actionpack (= 4.2.7.1) actionpack (= 4.2.9)
actionview (= 4.2.7.1) actionview (= 4.2.9)
activejob (= 4.2.7.1) activejob (= 4.2.9)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.7.1) actionpack (4.2.9)
actionview (= 4.2.7.1) actionview (= 4.2.9)
activesupport (= 4.2.7.1) activesupport (= 4.2.9)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.7.1) actionview (4.2.9)
activesupport (= 4.2.7.1) activesupport (= 4.2.9)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (4.2.7.1) activejob (4.2.9)
activesupport (= 4.2.7.1) activesupport (= 4.2.9)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.7.1) activemodel (4.2.9)
activesupport (= 4.2.7.1) activesupport (= 4.2.9)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.7.1) activerecord (4.2.9)
activemodel (= 4.2.7.1) activemodel (= 4.2.9)
activesupport (= 4.2.7.1) activesupport (= 4.2.9)
arel (~> 6.0) arel (~> 6.0)
activerecord-nulldb-adapter (0.3.6) activerecord-nulldb-adapter (0.3.7)
activerecord (>= 2.0.0) activerecord (>= 2.0.0)
activerecord-session_store (1.0.0) activerecord-session_store (1.1.0)
actionpack (>= 4.0, < 5.1) actionpack (>= 4.0, < 5.2)
activerecord (>= 4.0, < 5.1) activerecord (>= 4.0, < 5.2)
multi_json (~> 1.11, >= 1.11.2) multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3) rack (>= 1.5.2, < 3)
railties (>= 4.0, < 5.1) railties (>= 4.0, < 5.2)
activesupport (4.2.7.1) activesupport (4.2.9)
i18n (~> 0.7) i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1) tzinfo (~> 1.1)
addressable (2.4.0) addressable (2.4.0)
arel (6.0.3) arel (6.0.4)
argon2 (1.1.3) argon2 (1.1.3)
ffi (~> 1.9) ffi (~> 1.9)
ffi-compiler (~> 0.1) ffi-compiler (~> 0.1)
ast (2.3.0) ast (2.3.0)
autoprefixer-rails (6.4.1.1) autoprefixer-rails (7.1.2.4)
execjs execjs
biz (1.6.0) biz (1.7.0)
clavius (~> 1.0) clavius (~> 1.0)
tzinfo tzinfo
browser (2.2.0) browser (2.2.0)
buftok (0.2.0) buftok (0.2.0)
builder (3.2.2) builder (3.2.3)
childprocess (0.5.9) childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
clavius (1.0.2) clavius (1.0.2)
clearbit (0.2.5) clearbit (0.2.7)
nestful (~> 1.1.0) nestful (~> 1.1.0)
coderay (1.1.1) coderay (1.1.1)
coffee-rails (4.2.1) coffee-rails (4.2.1)
@ -70,31 +85,32 @@ GEM
coffee-script (2.4.1) coffee-script (2.4.1)
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.10.0) coffee-script-source (1.12.2)
coffeelint (1.14.0) coffeelint (1.14.0)
coffee-script coffee-script
execjs execjs
json json
concurrent-ruby (1.0.2) composite_primary_keys (8.1.6)
coveralls (0.8.16) activerecord (~> 4.2.0)
concurrent-ruby (1.0.5)
coveralls (0.8.21)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov (~> 0.12.0) simplecov (~> 0.14.1)
term-ansicolor (~> 1.3.0) term-ansicolor (~> 1.3)
thor (~> 0.19.1) thor (~> 0.19.4)
tins (>= 1.6.0, < 2) tins (~> 1.6)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
daemons (1.2.4) daemons (1.2.4)
delayed_job (4.1.2) delayed_job (4.1.3)
activesupport (>= 3.0, < 5.1) activesupport (>= 3.0, < 5.2)
delayed_job_active_record (4.1.1) delayed_job_active_record (4.1.2)
activerecord (>= 3.0, < 5.1) activerecord (>= 3.0, < 5.2)
delayed_job (>= 3.0, < 5) delayed_job (>= 3.0, < 5)
diff-lcs (1.2.5) diff-lcs (1.2.5)
diffy (3.1.0) diffy (3.1.0)
dnsruby (1.59.3)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20160826) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0) doorkeeper (4.2.0)
railties (>= 4.2) railties (>= 4.2)
@ -106,8 +122,6 @@ GEM
em-websocket (0.5.1) em-websocket (0.5.1)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
email_verifier (0.1.0)
dnsruby (>= 1.5)
equalizer (0.0.10) equalizer (0.0.10)
erubis (2.7.0) erubis (2.7.0)
eventmachine (1.2.3) eventmachine (1.2.3)
@ -136,8 +150,8 @@ GEM
rainbow (>= 2.1) rainbow (>= 2.1)
rake (>= 10.0) rake (>= 10.0)
retriable (~> 2.1) retriable (~> 2.1)
globalid (0.3.7) globalid (0.4.0)
activesupport (>= 4.1.0) activesupport (>= 4.2.0)
guard (2.14.0) guard (2.14.0)
formatador (>= 0.2.4) formatador (>= 0.2.4)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
@ -153,39 +167,49 @@ GEM
guard (~> 2.8) guard (~> 2.8)
guard-compat (~> 1.0) guard-compat (~> 1.0)
multi_json (~> 1.8) multi_json (~> 1.8)
guard-symlink (0.1.0) guard-symlink (0.1.1)
guard
guard-compat (~> 1.1) guard-compat (~> 1.1)
hashdiff (0.3.2) hashdiff (0.3.5)
hashie (3.4.4) hashie (3.5.6)
htmlentities (4.3.4) htmlentities (4.3.4)
http (1.0.4) http (1.0.4)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 1.0.1) http-form_data (~> 1.0.1)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
http-cookie (1.0.2) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (1.0.1) http-form_data (1.0.3)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
i18n (0.8.1) httpclient (2.8.3)
i18n (0.8.6)
icalendar (2.4.1) icalendar (2.4.1)
icalendar-recurrence (1.1.2)
icalendar (~> 2.0)
ice_cube (~> 0.13)
ice_cube (0.16.2)
inflection (1.0.0) inflection (1.0.0)
json (1.8.6) json (1.8.6)
jwt (1.5.4) jwt (1.5.6)
kgio (2.11.0) kgio (2.11.0)
koala (2.4.0) koala (2.4.0)
addressable addressable
faraday faraday
multi_json (>= 1.3.0) multi_json (>= 1.3.0)
libv8 (3.16.14.15) libv8 (3.16.14.19)
listen (3.1.5) listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2) ruby_dep (~> 1.2)
little-plugger (1.1.4)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
lumberjack (1.0.10) lumberjack (1.0.10)
mail (2.6.4) mail (2.6.6)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
@ -193,50 +217,54 @@ GEM
mime-types (3.1) mime-types (3.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mini_portile2 (2.1.0) mini_portile2 (2.2.0)
minitest (5.10.1) minitest (5.10.3)
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.5.5) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
mysql2 (0.4.4) mysql2 (0.4.6)
naught (1.1.0) naught (1.1.0)
nenv (0.3.0) nenv (0.3.0)
nestful (1.1.1) nestful (1.1.1)
net-ldap (0.15.0) net-ldap (0.15.0)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.7.1) nokogiri (1.8.0)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.2.0)
nori (2.6.0)
notiffany (0.1.1) notiffany (0.1.1)
nenv (~> 0.1) nenv (~> 0.1)
shellany (~> 0.0) shellany (~> 0.0)
oauth (0.5.1) oauth (0.5.1)
oauth2 (1.2.0) oauth2 (1.4.0)
faraday (>= 0.8, < 0.10) faraday (>= 0.8, < 0.13)
jwt (~> 1.0) jwt (~> 1.0)
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.4.1) octokit (4.4.1)
sawyer (~> 0.7.0, >= 0.5.3) sawyer (~> 0.7.0, >= 0.5.3)
omniauth (1.3.1) omniauth (1.6.1)
hashie (>= 1.2, < 4) hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.0, < 3) rack (>= 1.6.2, < 3)
omniauth-facebook (4.0.0) omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2) omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2) omniauth-github (1.3.0)
omniauth (~> 1.0) omniauth (~> 1.5)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2) omniauth-gitlab (1.0.2)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.4.1) omniauth-google-oauth2 (0.5.0)
jwt (~> 1.5.2) jwt (~> 1.5)
multi_json (~> 1.3) multi_json (~> 1.3)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.3.1) omniauth-oauth2 (>= 1.3.1)
omniauth-linkedin-oauth2 (0.1.5) omniauth-linkedin-oauth2 (0.1.5)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 omniauth-oauth2
omniauth-microsoft-office365 (0.0.7)
omniauth
omniauth-oauth2
omniauth-oauth (1.1.0) omniauth-oauth (1.1.0)
oauth oauth
omniauth (~> 1.0) omniauth (~> 1.0)
@ -248,32 +276,32 @@ GEM
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
parser (2.3.1.2) parser (2.3.1.2)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.20.0)
pluginator (1.3.0) pluginator (1.5.0)
power_assert (0.3.1) power_assert (0.3.1)
powerpack (0.1.1) powerpack (0.1.1)
pre-commit (0.28.0) pre-commit (0.35.0)
pluginator (~> 1.1) pluginator (~> 1.5)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
slop (~> 3.4) slop (~> 3.4)
puma (3.6.0) puma (3.9.1)
rack (1.6.4) rack (1.6.8)
rack-livereload (0.3.16) rack-livereload (0.3.16)
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.7.1) rails (4.2.9)
actionmailer (= 4.2.7.1) actionmailer (= 4.2.9)
actionpack (= 4.2.7.1) actionpack (= 4.2.9)
actionview (= 4.2.7.1) actionview (= 4.2.9)
activejob (= 4.2.7.1) activejob (= 4.2.9)
activemodel (= 4.2.7.1) activemodel (= 4.2.9)
activerecord (= 4.2.7.1) activerecord (= 4.2.9)
activesupport (= 4.2.7.1) activesupport (= 4.2.9)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.7.1) railties (= 4.2.9)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
@ -283,15 +311,16 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
rails-observers (0.1.2) rails-observers (0.1.5)
activemodel (~> 4.0) activemodel (>= 4.0)
railties (4.2.7.1) railties (4.2.9)
actionpack (= 4.2.7.1) actionpack (= 4.2.9)
activesupport (= 4.2.7.1) activesupport (= 4.2.9)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.2.2)
raindrops (0.17.0) rake
raindrops (0.19.0)
rake (12.0.0) rake (12.0.0)
rb-fsevent (0.9.7) rb-fsevent (0.9.7)
rb-inotify (0.9.7) rb-inotify (0.9.7)
@ -339,7 +368,6 @@ GEM
sawyer (0.7.0) sawyer (0.7.0)
addressable (>= 2.3.5, < 2.5) addressable (>= 2.3.5, < 2.5)
faraday (~> 0.8, < 0.10) faraday (~> 0.8, < 0.10)
scrub_rb (1.0.1)
selenium-webdriver (2.53.4) selenium-webdriver (2.53.4)
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.0) rubyzip (~> 1.0)
@ -347,19 +375,20 @@ GEM
shellany (0.0.1) shellany (0.0.1)
simple-rss (1.3.1) simple-rss (1.3.1)
simple_oauth (0.3.1) simple_oauth (0.3.1)
simplecov (0.12.0) simplecov (0.14.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.1)
simplecov-rcov (0.2.3) simplecov-rcov (0.2.3)
simplecov (>= 0.4.1) simplecov (>= 0.4.1)
slack-notifier (1.5.1) slack-notifier (1.5.1)
slop (3.6.0) slop (3.6.0)
spring (1.7.2) spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
sprockets (3.7.0) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.0) sprockets-rails (3.2.0)
@ -369,18 +398,18 @@ GEM
sqlite3 (1.3.11) sqlite3 (1.3.11)
telegramAPI (1.2.2) telegramAPI (1.2.2)
rest-client (~> 2.0, >= 1.7.3) rest-client (~> 2.0, >= 1.7.3)
term-ansicolor (1.3.2) term-ansicolor (1.6.0)
tins (~> 1.0) tins (~> 1.0)
test-unit (3.2.1) test-unit (3.2.1)
power_assert power_assert
therubyracer (0.12.2) therubyracer (0.12.3)
libv8 (~> 3.16.14.0) libv8 (~> 3.16.14.15)
ref ref
thor (0.19.1) thor (0.19.4)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.5) tilt (2.0.5)
tins (1.13.0) tins (1.15.0)
twitter (5.16.0) twitter (5.17.0)
addressable (~> 2.3) addressable (~> 2.3)
buftok (~> 0.2.0) buftok (~> 0.2.0)
equalizer (= 0.0.10) equalizer (= 0.0.10)
@ -391,30 +420,37 @@ GEM
memoizable (~> 0.4.0) memoizable (~> 0.4.0)
naught (~> 1.0) naught (~> 1.0)
simple_oauth (~> 0.3.0) simple_oauth (~> 0.3.0)
tzinfo (1.2.2) tzinfo (1.2.3)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.0.2) uglifier (3.0.2)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.2) unf_ext (0.0.7.4)
unicode-display_width (1.1.1) unicode-display_width (1.1.1)
unicorn (5.2.0) unicorn (5.3.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
webmock (2.3.2) valid_email2 (2.0.0)
activemodel (>= 3.2)
mail (~> 2.5)
viewpoint (1.1.0)
httpclient
logging
nokogiri
rubyntlm
webmock (3.0.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
websocket (1.2.3) websocket (1.2.3)
writeexcel (1.0.5) writeexcel (1.0.5)
zendesk_api (1.14.0) zendesk_api (1.14.4)
faraday (~> 0.9) faraday (~> 0.9)
hashie (>= 1.2, < 4.0, != 3.3.0) hashie (>= 3.5.2, < 4.0.0)
inflection inflection
mime-types mime-types
multipart-post (~> 2.0) multipart-post (~> 2.0)
scrub_rb (~> 1.0.1)
PLATFORMS PLATFORMS
ruby ruby
@ -423,6 +459,7 @@ DEPENDENCIES
activerecord-nulldb-adapter activerecord-nulldb-adapter
activerecord-session_store activerecord-session_store
argon2 argon2
autodiscover!
autoprefixer-rails autoprefixer-rails
biz biz
browser browser
@ -430,6 +467,7 @@ DEPENDENCIES
coffee-rails coffee-rails
coffee-script-source coffee-script-source
coffeelint coffeelint
composite_primary_keys
coveralls coveralls
daemons daemons
delayed_job_active_record delayed_job_active_record
@ -437,7 +475,6 @@ DEPENDENCIES
doorkeeper doorkeeper
eco eco
em-websocket em-websocket
email_verifier
eventmachine eventmachine
execjs execjs
factory_girl_rails factory_girl_rails
@ -448,6 +485,7 @@ DEPENDENCIES
guard-symlink guard-symlink
htmlentities htmlentities
icalendar icalendar
icalendar-recurrence
json json
koala koala
libv8 libv8
@ -462,17 +500,19 @@ DEPENDENCIES
omniauth-gitlab omniauth-gitlab
omniauth-google-oauth2 omniauth-google-oauth2
omniauth-linkedin-oauth2 omniauth-linkedin-oauth2
omniauth-microsoft-office365
omniauth-oauth2 omniauth-oauth2
omniauth-twitter omniauth-twitter
pg pg
pre-commit pre-commit
puma puma
rack-livereload rack-livereload
rails (= 4.2.7.1) rails (= 4.2.9)
rails-observers rails-observers
rb-fsevent rb-fsevent
rspec-rails rspec-rails
rubocop rubocop
rubyntlm!
sass-rails sass-rails
selenium-webdriver selenium-webdriver
simple-rss simple-rss
@ -489,12 +529,14 @@ DEPENDENCIES
twitter twitter
uglifier uglifier
unicorn unicorn
valid_email2
viewpoint
webmock webmock
writeexcel writeexcel
zendesk_api zendesk_api
RUBY VERSION RUBY VERSION
ruby 2.3.1p112 ruby 2.4.1p111
BUNDLED WITH BUNDLED WITH
1.13.7 1.15.3

View file

@ -9,7 +9,7 @@ with a team of agents?
You're going to love Zammad! You're going to love Zammad!
## Statusbadges ## Status
- Build: [![Build Status](https://travis-ci.org/zammad/zammad.svg?branch=develop)](https://travis-ci.org/zammad/zammad) - Build: [![Build Status](https://travis-ci.org/zammad/zammad.svg?branch=develop)](https://travis-ci.org/zammad/zammad)
- Code: [![Code Climate](https://codeclimate.com/github/zammad/zammad/badges/gpa.svg)](https://codeclimate.com/github/zammad/zammad) [![Coverage Status](https://coveralls.io/repos/github/zammad/zammad/badge.svg)](https://coveralls.io/github/zammad/zammad) - Code: [![Code Climate](https://codeclimate.com/github/zammad/zammad/badges/gpa.svg)](https://codeclimate.com/github/zammad/zammad) [![Coverage Status](https://coveralls.io/repos/github/zammad/zammad/badge.svg)](https://coveralls.io/github/zammad/zammad)

View file

@ -1 +1 @@
1.6.x 2.1.x

View file

@ -61,13 +61,13 @@ class App.Controller extends Spine.Controller
clearDelay: (delay_id) => clearDelay: (delay_id) =>
App.Delay.clear(delay_id, @controllerId) App.Delay.clear(delay_id, @controllerId)
delay: (callback, timeout, delay_id, queue = true) => delay: (callback, timeout, delay_id, queue = false) =>
App.Delay.set(callback, timeout, delay_id, @controllerId, queue) App.Delay.set(callback, timeout, delay_id, @controllerId, queue)
clearInterval: (interval_id) => clearInterval: (interval_id) =>
App.Interval.clear(interval_id, @controllerId) App.Interval.clear(interval_id, @controllerId)
interval: (callback, interval, interval_id, queue = true) => interval: (callback, interval, interval_id, queue = false) =>
App.Interval.set(callback, interval, interval_id, @controllerId, queue) App.Interval.set(callback, interval, interval_id, @controllerId, queue)
releaseController: => releaseController: =>
@ -185,6 +185,17 @@ class App.Controller extends Spine.Controller
formValidate: (data) -> formValidate: (data) ->
App.ControllerForm.validate(data) App.ControllerForm.validate(data)
# get all query params of the url
queryParam: ->
return if !@query
pairs = @query.split(';')
params = {}
for pair in pairs
result = pair.match('(.+?)=(.*)')
if result && result[1]
params[result[1]] = result[2]
params
# redirectToLogin: (data) -> # redirectToLogin: (data) ->
# #
@ -344,7 +355,10 @@ class App.Controller extends Spine.Controller
title: -> title: ->
userId = $(@).data('id') userId = $(@).data('id')
user = App.User.find(userId) user = App.User.find(userId)
App.Utils.htmlEscape(user.displayName()) headline = App.Utils.htmlEscape(user.displayName())
if user.isOutOfOffice()
headline += " (#{App.Utils.htmlEscape(user.outOfOfficeText())})"
headline
content: -> content: ->
userId = $(@).data('id') userId = $(@).data('id')
user = App.User.fullLocal(userId) user = App.User.fullLocal(userId)

View file

@ -229,7 +229,7 @@ class App.ControllerForm extends App.Controller
if attribute.type is 'hidden' if attribute.type is 'hidden'
attribute.autocomplete = '' attribute.autocomplete = ''
else else
attribute.autocomplete = 'autocomplete="new-password"' attribute.autocomplete = 'autocomplete="off"'
else else
attribute.autocomplete = 'autocomplete="' + attribute.autocomplete + '"' attribute.autocomplete = 'autocomplete="' + attribute.autocomplete + '"'
@ -426,8 +426,11 @@ class App.ControllerForm extends App.Controller
delete param[item.name] delete param[item.name]
continue continue
# collect all params, push it to an array if already exists # collect all params, push it to an array item.value already exists
value = item.value.trim() value = item.value
if item.value
value = item.value.trim()
if item.type is 'boolean' if item.type is 'boolean'
if value is '' if value is ''
value = undefined value = undefined

View file

@ -309,6 +309,23 @@ class App.ControllerConfirm extends App.ControllerModal
if @callback if @callback
@callback() @callback()
class App.ControllerErrorModal extends App.ControllerModal
buttonClose: true
buttonCancel: false
buttonSubmit: 'Close'
#buttonClass: 'btn--danger'
head: 'Error'
#small: true
#shown: true
content: ->
@message
onSubmit: =>
@close()
if @callback
@callback()
class App.ControllerDrox extends App.Controller class App.ControllerDrox extends App.Controller
constructor: (params) -> constructor: (params) ->
super super
@ -659,8 +676,9 @@ class App.Sidebar extends App.Controller
render: => render: =>
localEl = $(App.view('generic/sidebar_tabs')( localEl = $(App.view('generic/sidebar_tabs')(
items: @items items: @items
scrollbarWidth: App.Utils.getScrollBarWidth() scrollbarWidth: App.Utils.getScrollBarWidth()
dir: App.i18n.dir()
)) ))
# init content callback # init content callback

View file

@ -21,7 +21,6 @@ class App.ChannelChat extends App.ControllerSubContent
'.js-chat-welcome': 'chatWelcome' '.js-chat-welcome': 'chatWelcome'
'.js-testurl-input': 'urlInput' '.js-testurl-input': 'urlInput'
'.js-backgroundColor': 'chatBackground' '.js-backgroundColor': 'chatBackground'
'.js-paramsBlock': 'paramsBlock'
'.js-code': 'code' '.js-code': 'code'
'.js-palette': 'palette' '.js-palette': 'palette'
'.js-color': 'colorField' '.js-color': 'colorField'
@ -361,7 +360,7 @@ class App.ChannelChat extends App.ControllerSubContent
@$('.js-modal-params').html(paramString) @$('.js-modal-params').html(paramString)
# highlight # highlight
@paramsBlock.each (i, block) -> @code.each (i, block) ->
hljs.highlightBlock block hljs.highlightBlock block
App.Config.set('Chat', { prio: 4000, name: 'Chat', parent: '#channels', target: '#channels/chat', controller: App.ChannelChat, permission: ['admin.chat'] }, 'NavBarAdmin') App.Config.set('Chat', { prio: 4000, name: 'Chat', parent: '#channels', target: '#channels/chat', controller: App.ChannelChat, permission: ['admin.chat'] }, 'NavBarAdmin')

View file

@ -45,9 +45,7 @@ class App.ChannelEmailFilter extends App.Controller
template = $( '<div><div class="overview"></div><a data-type="new" class="btn btn--success">' + App.i18n.translateContent('New') + '</a></div>' ) template = $( '<div><div class="overview"></div><a data-type="new" class="btn btn--success">' + App.i18n.translateContent('New') + '</a></div>' )
description = ''' description = 'With filters you can e. g. dispatch new tickets into certain groups or set a certain priority for tickets of a VIP customer.'
With Filters you can e. g. dispatch new Tickets into certain groups or set a certain priority for Tickets of an VIP customer.
'''
new App.ControllerTable( new App.ControllerTable(
el: template.find('.overview') el: template.find('.overview')
@ -110,7 +108,7 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
# show errors in form # show errors in form
if errors if errors
@log 'error', errors @log 'error', errors
@formValidate( form: e.target, errors: errors ) @formValidate(form: e.target, errors: errors)
return false return false
# disable form # disable form
@ -120,8 +118,10 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
object.save( object.save(
done: => done: =>
@close() @close()
fail: => fail: (settings, details) =>
@close() @log 'errors', details
@formEnable(e)
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
) )
class App.ChannelEmailSignature extends App.Controller class App.ChannelEmailSignature extends App.Controller
@ -203,7 +203,7 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
# show errors in form # show errors in form
if errors if errors
@log 'error', errors @log 'error', errors
@formValidate( form: e.target, errors: errors ) @formValidate(form: e.target, errors: errors)
return false return false
# disable form # disable form
@ -213,8 +213,10 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
object.save( object.save(
done: => done: =>
@close() @close()
fail: => fail: (settings, details) =>
@log 'errors', details
@formEnable(e) @formEnable(e)
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
) )
class App.ChannelEmailAccountOverview extends App.Controller class App.ChannelEmailAccountOverview extends App.Controller
@ -533,8 +535,8 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# base # base
configureAttributesBase = [ configureAttributesBase = [
{ name: 'realname', display: 'Organization & Department Name', tag: 'input', type: 'text', limit: 160, null: false, placeholder: 'Organization Support', autocomplete: 'new-password' }, { name: 'realname', display: 'Organization & Department Name', tag: 'input', type: 'text', limit: 160, null: false, placeholder: 'Organization Support', autocomplete: 'off' },
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'new-password' }, { name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, autocomplete: 'off' },
{ name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'group_id', display: 'Destination Group', tag: 'select', null: false, relation: 'Group', nulloption: true }, { name: 'group_id', display: 'Destination Group', tag: 'select', null: false, relation: 'Group', nulloption: true },
] ]
@ -562,21 +564,24 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# inbound # inbound
configureAttributesInbound = [ configureAttributesInbound = [
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off' },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false }, { name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' },
{ name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
] ]
showHideFolder = (params, attribute, attributes, classname, form, ui) -> showHideFolder = (params, attribute, attributes, classname, form, ui) ->
return if !params return if !params
if params.adapter is 'imap' if params.adapter is 'imap'
ui.show('options::folder') ui.show('options::folder')
ui.show('options::keep_on_server')
return return
ui.hide('options::folder') ui.hide('options::folder')
ui.hide('options::keep_on_server')
handlePort = (params, attribute, attributes, classname, form, ui) -> handlePort = (params, attribute, attributes, classname, form, ui) ->
return if !params return if !params
@ -608,9 +613,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# fill user / password based on intro info # fill user / password based on intro info
channel_used = { options: {} } channel_used = { options: {} }
if @account['meta'] if @account['meta']
channel_used['options']['user'] = @account['meta']['email'] channel_used['options']['user'] = @account['meta']['email']
channel_used['options']['password'] = @account['meta']['password'] channel_used['options']['password'] = @account['meta']['password']
channel_used['options']['folder'] = @account['meta']['folder'] channel_used['options']['folder'] = @account['meta']['folder']
channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server']
# show used backend # show used backend
@$('.base-outbound-settings').html('') @$('.base-outbound-settings').html('')
@ -618,7 +624,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
if adapter is 'smtp' if adapter is 'smtp'
configureAttributesOutbound = [ configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
] ]
@ -672,7 +678,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
for key, value of data.setting for key, value of data.setting
@account[key] = value @account[key] = value
if data.content_messages && data.content_messages > 0 if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
@$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-message').html(message)
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro') @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
@ -726,7 +732,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# remember account settings # remember account settings
@account.inbound = params @account.inbound = params
if data.content_messages && data.content_messages > 0 if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
@$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-message').html(message)
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound') @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
@ -932,7 +938,7 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
if adapter is 'smtp' if adapter is 'smtp'
configureAttributesOutbound = [ configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password' }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off' },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
] ]

View file

@ -3,12 +3,14 @@ class App.ChannelForm extends App.ControllerSubContent
requiredPermission: 'admin.channel_formular' requiredPermission: 'admin.channel_formular'
header: 'Form' header: 'Form'
events: events:
'change form.js-params': 'updateParams' 'change form.js-paramsDesigner': 'updateParamsDesigner'
'keyup form.js-params': 'updateParams' 'keyup form.js-paramsDesigner': 'updateParamsDesigner'
'change .js-formSetting input': 'toggleFormSetting' 'change .js-formSetting input': 'toggleFormSetting'
'change .js-paramsSetting select': 'updateGroup'
elements: elements:
'.js-paramsBlock': 'paramsBlock' '.js-code': 'code'
'.js-paramsSetting': 'paramsSetting'
'.js-formSetting input': 'formSetting' '.js-formSetting input': 'formSetting'
constructor: -> constructor: ->
@ -20,22 +22,38 @@ class App.ChannelForm extends App.ControllerSubContent
render: => render: =>
setting = App.Setting.get('form_ticket_create') setting = App.Setting.get('form_ticket_create')
@html App.view('channel/form')(
element = $(App.view('channel/form')(
baseurl: window.location.origin baseurl: window.location.origin
formSetting: setting formSetting: setting
) ))
@paramsBlock.each (i, block) -> group_id = App.Setting.get('form_ticket_create_group_id')
selection = App.UiElement.select.render(
name: 'group_id'
multiple: false
null: false
relation: 'Group'
nulloption: false
value: group_id
#class: 'form-control--small'
)
console.log('s', element.find('.js-groupSelector'), selection)
element.find('.js-groupSelector').html(selection)
@html element
@code.each (i, block) ->
hljs.highlightBlock block hljs.highlightBlock block
@updateParams() @updateParamsDesigner()
updateParams: -> updateParamsDesigner: ->
quote = (string) -> quote = (string) ->
string = string.replace('\'', '\\\'') string = string.replace('\'', '\\\'')
.replace(/\</g, '&lt;') .replace(/\</g, '&lt;')
.replace(/\>/g, '&gt;') .replace(/\>/g, '&gt;')
params = @formParam(@$('.js-params')) params = @formParam(@$('.js-paramsDesigner'))
paramString = '' paramString = ''
for key, value of params for key, value of params
if value != '' if value != ''
@ -63,4 +81,8 @@ class App.ChannelForm extends App.ControllerSubContent
value = @formSetting.prop('checked') value = @formSetting.prop('checked')
App.Setting.set('form_ticket_create', value) App.Setting.set('form_ticket_create', value)
updateGroup: =>
value = @paramsSetting.find('[name=group_id]').val()
App.Setting.set('form_ticket_create_group_id', value)
App.Config.set('Form', { prio: 2000, name: 'Form', parent: '#channels', target: '#channels/form', controller: App.ChannelForm, permission: ['admin.formular'] }, 'NavBarAdmin') App.Config.set('Form', { prio: 2000, name: 'Form', parent: '#channels', target: '#channels/form', controller: App.ChannelForm, permission: ['admin.formular'] }, 'NavBarAdmin')

View file

@ -0,0 +1,46 @@
class Index extends App.ControllerIntegrationBase
featureIntegration: 'check_mk_integration'
featureName: 'Check_MK'
featureConfig: 'check_mk_config'
description: [
['This service receives http requests from %s and creates tickets with host and service.', 'Check_MK']
['If the host and service is recovered again, the ticket will be closed automatically.']
]
render: =>
super
new App.SettingsForm(
area: 'Integration::CheckMK'
el: @$('.js-form')
)
new App.ScriptSnipped(
el: @$('.js-scriptSnipped')
facility: 'check_mk'
style: 'bash'
content: "#!/bin/bash\n\ncurl -X POST -F 'event_id=123' -F 'host=host1' -F 'service=http' -F 'state=down' #{App.Config.get('http_type')}://#{App.Config.get('fqdn')}/api/v1/integration/check_mk/#{App.Setting.get('check_mk_token')}"
description: [
['To enable %s for sending http requests to %s, you need create "%s" in the admin interface if %s.', 'Check_MK', 'Zammad', 'Event Actions', 'Check_MK']
]
)
new App.HttpLog(
el: @$('.js-log')
facility: 'check_mk'
)
class State
@current: ->
App.Setting.get('check_mk_integration')
App.Config.set(
'IntegrationCheckMk'
{
name: 'Check_MK'
target: '#system/integration/check_mk'
description: 'An open source monitoring tool.'
controller: Index
state: State
}
'NavBarIntegrations'
)

View file

@ -0,0 +1,528 @@
class Index extends App.ControllerIntegrationBase
featureIntegration: 'exchange_integration'
featureName: 'Exchange'
featureConfig: 'exchange_config'
description: [
['This service enables Zammad to connect with your Exchange server.']
]
events:
'change .js-switch input': 'switch'
render: =>
super
new Form(
el: @$('.js-form')
)
#new App.ImportJob(
# el: @$('.js-importJob')
# facility: 'exchange'
#)
new App.HttpLog(
el: @$('.js-log')
facility: 'exchange'
)
switch: =>
super
active = @$('.js-switch input').prop('checked')
if active
job_start = =>
@ajax(
id: 'jobs_config'
type: 'POST'
url: "#{@apiPath}/integration/exchange/job_start"
processData: true
success: (data, status, xhr) =>
@render(true)
)
App.Delay.set(
job_start,
600,
'job_start',
)
class Form extends App.Controller
elements:
'.js-lastImport': 'lastImport'
'.js-wizard': 'wizardButton'
events:
'click .js-wizard': 'startWizard'
'click .js-start-sync': 'startSync'
constructor: ->
super
@render()
@lastResult()
@activeDryRun()
currentConfig: ->
App.Setting.get('exchange_config') || {}
setConfig: (value) =>
App.Setting.set('exchange_config', value, {notify: true})
@startSync()
render: (top = false) =>
@config = @currentConfig()
folders = []
if !_.isEmpty(@config.folders)
for folder_id in @config.folders
folders.push @config.wizardData.backend_folders[folder_id]
@html App.view('integration/exchange')(
config: @config,
folders: folders
)
if _.isEmpty(@config)
@$('.js-notConfigured').removeClass('hide')
@$('.js-summary').addClass('hide')
else
@$('.js-notConfigured').addClass('hide')
@$('.js-summary').removeClass('hide')
if top
a = =>
@scrollToIfNeeded($('.content.active .page-header'))
@delay(a, 500)
startSync: =>
@ajax(
id: 'jobs_config'
type: 'POST'
url: "#{@apiPath}/integration/exchange/job_start"
processData: true
success: (data, status, xhr) =>
@render(true)
@lastResult()
)
startWizard: (e) =>
e.preventDefault()
new ConnectionWizard(
container: @el.closest('.content')
config: @config
callback: (config) =>
@setConfig(config)
)
lastResult: =>
@ajax(
id: 'jobs_start_index'
type: 'GET'
url: "#{@apiPath}/integration/exchange/job_start"
processData: true
success: (job, status, xhr) =>
if !_.isEmpty(job)
if !@lastResultShowJob || @lastResultShowJob.updated_at != job.updated_at
@lastResultShowJob = job
@lastResultShow(job)
if job.finished_at
@wizardButton.attr('disabled', false)
else
@wizardButton.attr('disabled', true)
@delay(@lastResult, 5000)
)
lastResultShow: (job) =>
if _.isEmpty(job)
@lastImport.html('')
return
countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped + job.result.failed
if !job.result.roles
job.result.roles = {}
for role_id, statistic of job.result.role_ids
role = App.Role.find(role_id)
job.result.roles[role.displayName()] = statistic
el = $(App.view('integration/exchange_last_import')(job: job, countDone: countDone))
@lastImport.html(el)
activeDryRun: =>
@ajax(
id: 'jobs_try_index'
type: 'GET'
url: "#{@apiPath}/integration/exchange/job_try"
data:
finished: false
processData: true
success: (job, status, xhr) =>
return if _.isEmpty(job)
# show analyzing
new ConnectionWizard(
container: @el.closest('.content')
config: job.payload
start: 'tryLoop'
callback: (config) =>
@wizardButton.attr('disabled', false)
@setConfig(config)
)
@wizardButton.attr('disabled', true)
)
class State
@current: ->
App.Setting.get('exchange_integration')
class ConnectionWizard extends App.WizardModal
wizardConfig: {}
slideMethod:
'js-folders': 'foldersShow'
'js-mapping': 'mappingShow'
events:
'submit form.js-discover': 'discover'
'submit form.js-bind': 'folders'
'submit form.js-folders': 'mapping'
'click .js-mapping .js-submitTry': 'mappingChange'
'click .js-try .js-submitSave': 'save'
'click .js-close': 'hide'
'click .js-remove': 'removeRow'
'click .js-userMappingForm .js-add': 'addUserMapping'
'click .js-goToSlide': 'goToSlide'
elements:
'.modal-body': 'body'
'.js-foldersSelect': 'foldersSelect'
'.js-folders .js-submitTry': 'foldersSelectSubmit'
'.js-userMappingForm': 'userMappingForm'
'.js-expertForm': 'expertForm'
constructor: ->
super
if !_.isEmpty(@config)
@wizardConfig = @config
if @container
@el.addClass('modal--local')
@render()
@el.modal
keyboard: true
show: true
backdrop: true
container: @container
.on
'show.bs.modal': @onShow
'shown.bs.modal': @onShown
'hidden.bs.modal': =>
@el.remove()
if @slide
@showSlide(@slide)
else
@showDiscoverDetails()
if @start
@[@start]()
render: =>
@html App.view('integration/exchange_wizard')()
save: (e) =>
e.preventDefault()
@callback(@wizardConfig)
@hide(e)
showSlide: (slide) =>
method = @slideMethod[slide]
if method && @[method]
@[method](true)
super
showDiscoverDetails: =>
@$('.js-discover input[name="user"]').val(@wizardConfig.user)
@$('.js-discover input[name="password"]').val(@wizardConfig.password)
showBindDetails: =>
@$('.js-bind input[name="endpoint"]').val(@wizardConfig.endpoint)
@$('.js-bind input[name="user"]').val(@wizardConfig.user)
@$('.js-bind input[name="password"]').val(@wizardConfig.password)
discover: (e) =>
e.preventDefault()
@showSlide('js-connect')
params = @formParam(e.target)
@ajax(
id: 'exchange_discover'
type: 'POST'
url: "#{@apiPath}/integration/exchange/autodiscover"
data: JSON.stringify(params)
processData: true
success: (data, status, xhr) =>
if data.result isnt 'ok'
@showSlide('js-discover')
@showAlert('js-discover', data.message)
return
@wizardConfig.endpoint = data.endpoint
@wizardConfig.user = params.user
@wizardConfig.password = params.password
@showSlide('js-bind')
@showBindDetails()
error: (xhr, statusText, error) =>
detailsRaw = xhr.responseText
details = {}
if !_.isEmpty(detailsRaw)
details = JSON.parse(detailsRaw)
@showSlide('js-discover')
@showAlert('js-discover', details.error || 'Unable to perform backend.')
)
folders: (e) =>
e.preventDefault()
@showSlide('js-analyze')
params = @formParam(e.target)
@ajax(
id: 'exchange_folders'
type: 'POST'
url: "#{@apiPath}/integration/exchange/folders"
data: JSON.stringify(params)
processData: true
success: (data, status, xhr) =>
if data.result isnt 'ok'
@showSlide('js-bind')
@showAlert('js-bind', data.message)
return
@wizardConfig.endpoint = params.endpoint
@wizardConfig.user = params.user
@wizardConfig.password = params.password
# update wizard data
@wizardConfig.wizardData = {}
@wizardConfig.wizardData.backend_folders = data.folders
@foldersShow()
error: (xhr, statusText, error) =>
detailsRaw = xhr.responseText
details = {}
if !_.isEmpty(detailsRaw)
details = JSON.parse(detailsRaw)
@showSlide('js-bind')
@showAlert('js-bind', details.error || 'Unable to perform backend.')
)
foldersShow: (alreadyShown) =>
@showSlide('js-folders') if !alreadyShown
@foldersSelect.html(@createColumnSelection('folders', @wizardConfig.wizardData.backend_folders, @wizardConfig.folders))
createColumnSelection: (name, options, selected) ->
return App.UiElement.column_select.render(
name: name
null: false
nulloption: false
options: options
value: selected
onChange: (val) =>
if val && val.length > 0
@foldersSelectSubmit.removeClass('is-disabled')
else
@foldersSelectSubmit.addClass('is-disabled')
)
mapping: (e) =>
e.preventDefault()
@showSlide('js-analyze')
params = @formParam(e.target)
# folders might be a single selection so we
# have to ensure that is an Array so the
# backend and frontend can handle it properly
if typeof params.folders is 'string'
params.folders = [ params.folders ]
# add login params
params.endpoint = @wizardConfig.endpoint
params.user = @wizardConfig.user
params.password = @wizardConfig.password
@ajax(
id: 'exchange_mapping'
type: 'POST'
url: "#{@apiPath}/integration/exchange/mapping"
data: JSON.stringify(params)
processData: true
success: (data, status, xhr) =>
if data.result isnt 'ok'
@showSlide('js-folders')
@showAlert('js-folders', data.message)
return
attributes = {}
for key, value of App.User.attributesGet()
continue if key == 'login'
if (value.tag is 'input' || value.tag is 'richtext' || value.tag is 'textarea') && value.type isnt 'password'
attributes[key] = value.display || key
@wizardConfig.wizardData.attributes = attributes
@wizardConfig.folders = params.folders
@wizardConfig.wizardData.backend_attributes = data.attributes
@mappingShow()
error: (xhr, statusText, error) =>
detailsRaw = xhr.responseText
details = {}
if !_.isEmpty(detailsRaw)
details = JSON.parse(detailsRaw)
@showSlide('js-folders')
@showAlert('js-folders', details.error || 'Unable to perform backend.')
)
mappingShow: (alreadyShown) =>
@showSlide('js-mapping') if !alreadyShown
user_attribute_map = @wizardConfig.attributes
if _.isEmpty(user_attribute_map)
user_attribute_map =
given_name: 'firstname'
surname: 'lastname'
'email_addresses.emailaddress1': 'email'
'phone_numbers.businessphone': 'phone'
@userMappingForm.find('tbody tr.js-entry').remove()
@userMappingForm.find('tbody tr').before(@buildRowsUserMap(user_attribute_map))
mappingChange: (e) =>
e.preventDefault()
# user map
attributes = @formParam(@userMappingForm)
for key in ['source', 'dest']
if !_.isArray(attributes[key])
attributes[key] = [attributes[key]]
attributes_local =
item_id: 'login'
length = attributes.source.length-1
for count in [0..length]
if attributes.source[count] && attributes.dest[count]
attributes_local[attributes.source[count]] = attributes.dest[count]
@wizardConfig.attributes = attributes_local
@tryShow()
buildRowsUserMap: (user_attribute_map) =>
# show static login row
userUidDisplayValue = @wizardConfig.wizardData.backend_attributes['item_id']
el = [
$(App.view('integration/ldap_user_attribute_row_read_only')(
key: userUidDisplayValue,
value: 'Login'
))
]
for source, dest of user_attribute_map
continue if source == 'item_id'
continue if !(source of @wizardConfig.wizardData.backend_attributes)
el.push @buildRowUserAttribute(source, dest)
el
buildRowUserAttribute: (source, dest) =>
el = $(App.view('integration/exchange_user_attribute_row')())
el.find('.js-exchangeAttribute').html(@createSelection('source', @wizardConfig.wizardData.backend_attributes, source))
el.find('.js-userAttribute').html(@createSelection('dest', @wizardConfig.wizardData.attributes, dest))
el
createSelection: (name, options, selected, unknown) ->
return App.UiElement.searchable_select.render(
name: name
multiple: false
limit: 100
null: false
nulloption: false
options: options
value: selected
unknown: unknown
class: 'form-control--small'
)
removeRow: (e) ->
e.preventDefault()
$(e.target).closest('tr').remove()
addUserMapping: (e) =>
e.preventDefault()
@userMappingForm.find('tbody tr').last().before(@buildRowUserAttribute())
tryShow: (e) =>
if e
e.preventDefault()
@showSlide('js-analyze')
# create import job
@ajax(
id: 'exchange_try'
type: 'POST'
url: "#{@apiPath}/integration/exchange/job_try"
data: JSON.stringify(@wizardConfig)
processData: true
success: (data, status, xhr) =>
@tryLoop()
)
tryLoop: =>
@showSlide('js-dry')
@ajax(
id: 'jobs_try_index'
type: 'GET'
url: "#{@apiPath}/integration/exchange/job_try"
data:
finished: true
processData: true
success: (job, status, xhr) =>
if job.result && (job.result.error || job.result.info)
@showSlide('js-error')
@showAlert('js-error', (job.result.error || job.result.info))
return
total = 0
if job.result && _.keys(job.result).length > 0
@$('.js-preprogress').addClass('hide')
@$('.js-analyzing').removeClass('hide')
analized = 0
total = job.result.sum
for action, sum of job.result
continue if action == 'folders'
continue if action == 'sum'
analized += sum
@$('.js-progress progress').attr('value', analized)
@$('.js-progress progress').attr('max', total)
if job.finished_at
# reset initial state in case the back button is used
@$('.js-preprogress').removeClass('hide')
@$('.js-analyzing').addClass('hide')
@tryResult(job, total)
else
@delay(@tryLoop, 4000)
)
tryResult: (job, total) =>
@showSlide('js-try')
el = $(App.view('integration/exchange_summary')(job: job, countDone: total))
@el.find('.js-summary').html(el)
App.Config.set(
'IntegrationExchange'
{
name: 'Exchange'
target: '#system/integration/exchange'
description: 'Exchange integration for contacts management.'
controller: Index
state: State
}
'NavBarIntegrations'
)

View file

@ -0,0 +1,94 @@
class Index extends App.ControllerIntegrationBase
featureIntegration: 'idoit_integration'
featureName: 'i-doit'
featureConfig: 'idoit_config'
description: [
['This service allows you to connect i-doit objects with Zammad.']
]
events:
'change .js-switch input': 'switch'
render: =>
super
new Form(
el: @$('.js-form')
)
new App.HttpLog(
el: @$('.js-log')
facility: 'idoit'
)
class Form extends App.Controller
events:
'submit form': 'update'
constructor: ->
super
@render()
currentConfig: ->
App.Setting.get('idoit_config')
setConfig: (value) ->
App.Setting.set('idoit_config', value, {notify: true})
render: =>
@config = @currentConfig()
@html App.view('integration/idoit')(
config: @config
)
update: (e) =>
e.preventDefault()
@config = @formParam(e.target)
@validateAndSave()
validateAndSave: =>
@ajax(
id: 'idoit'
type: 'POST'
url: "#{@apiPath}/integration/idoit/verify"
data: JSON.stringify(
method: 'cmdb.object_types'
api_token: @config.api_token
endpoint: @config.endpoint
client_id: @config.client_id
)
success: (data, status, xhr) =>
if data.result is 'failed'
new App.ControllerErrorModal(
message: data.message
container: @el.closest('.content')
)
return
@setConfig(@config)
error: (data, status) =>
# do not close window if request is aborted
return if status is 'abort'
details = data.responseJSON || {}
@notify(
type: 'error'
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to save!')
)
)
class State
@current: ->
App.Setting.get('idoit_integration')
App.Config.set(
'IntegrationIdoit'
{
name: 'i-doit'
target: '#system/integration/idoit'
description: 'CMDB to document complex relations of your network components.'
controller: Index
state: State
}
'NavBarIntegrations'
)

View file

@ -28,13 +28,20 @@ class Index extends App.ControllerIntegrationBase
super super
active = @$('.js-switch input').prop('checked') active = @$('.js-switch input').prop('checked')
if active if active
@ajax( job_start = =>
id: 'jobs_config' @ajax(
type: 'POST' id: 'jobs_config'
url: "#{@apiPath}/integration/ldap/job_start" type: 'POST'
processData: true url: "#{@apiPath}/integration/ldap/job_start"
success: (data, status, xhr) => processData: true
@render(true) success: (data, status, xhr) =>
@render(true)
)
App.Delay.set(
job_start,
600,
'job_start',
) )
class Form extends App.Controller class Form extends App.Controller
@ -61,8 +68,15 @@ class Form extends App.Controller
render: (top = false) => render: (top = false) =>
@config = @currentConfig() @config = @currentConfig()
group_role_map = {}
for source, dests of @config.group_role_map
group_role_map[source] = dests.map((dest) ->
App.Role.find(dest).displayName()
).join ', '
@html App.view('integration/ldap')( @html App.view('integration/ldap')(
config: @config config: @config,
group_role_map: group_role_map
) )
if _.isEmpty(@config) if _.isEmpty(@config)
@$('.js-notConfigured').removeClass('hide') @$('.js-notConfigured').removeClass('hide')
@ -84,6 +98,7 @@ class Form extends App.Controller
processData: true processData: true
success: (data, status, xhr) => success: (data, status, xhr) =>
@render(true) @render(true)
@lastResult()
) )
startWizard: (e) => startWizard: (e) =>
@ -280,12 +295,13 @@ class ConnectionWizard extends App.WizardModal
option = '' option = ''
options = {} options = {}
for dn in data.attributes.namingcontexts if !_.isEmpty data.attributes
options[dn] = dn for dn in data.attributes.namingcontexts
if option is '' options[dn] = dn
option = dn if option is ''
if option.length > dn.length option = dn
option = dn if option.length > dn.length
option = dn
@wizardConfig.options = options @wizardConfig.options = options
@wizardConfig.option = option @wizardConfig.option = option
@ -419,7 +435,9 @@ class ConnectionWizard extends App.WizardModal
length = group_role_map.source.length-1 length = group_role_map.source.length-1
for count in [0..length] for count in [0..length]
if group_role_map.source[count] && group_role_map.dest[count] if group_role_map.source[count] && group_role_map.dest[count]
group_role_map_local[group_role_map.source[count]] = group_role_map.dest[count] if !_.isArray(group_role_map_local[group_role_map.source[count]])
group_role_map_local[group_role_map.source[count]] = []
group_role_map_local[group_role_map.source[count]].push group_role_map.dest[count]
@wizardConfig.group_role_map = group_role_map_local @wizardConfig.group_role_map = group_role_map_local
expertSettings = @formParam(@expertForm) expertSettings = @formParam(@expertForm)
@ -454,8 +472,9 @@ class ConnectionWizard extends App.WizardModal
buildRowsGroupRole: (group_role_map) => buildRowsGroupRole: (group_role_map) =>
el = [] el = []
for source, dest of group_role_map for source, dests of group_role_map
el.push @buildRowGroupRole(source, dest) for dest in dests
el.push @buildRowGroupRole(source, dest)
el el
buildRowGroupRole: (source, dest) => buildRowGroupRole: (source, dest) =>

View file

@ -9,43 +9,7 @@ class Index extends App.ControllerSubContent
@render() @render()
render: => render: =>
auth_provider_all = { auth_provider_all = App.Config.get('auth_provider_all')
facebook: {
url: '/auth/facebook'
name: 'Facebook'
config: 'auth_facebook'
},
twitter: {
url: '/auth/twitter'
name: 'Twitter'
config: 'auth_twitter'
},
linkedin: {
url: '/auth/linkedin'
name: 'LinkedIn'
config: 'auth_linkedin'
},
github: {
url: '/auth/github'
name: 'GitHub'
config: 'auth_github'
},
gitlab: {
url: '/auth/gitlab'
name: 'GitLab'
config: 'auth_gitlab'
},
google_oauth2: {
url: '/auth/google_oauth2'
name: 'Google'
config: 'auth_google_oauth2'
},
oauth2: {
url: '/auth/oauth2'
name: 'OAuth2'
config: 'auth_oauth2'
},
}
auth_providers = {} auth_providers = {}
for key, provider of auth_provider_all for key, provider of auth_provider_all
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true' if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
@ -90,3 +54,45 @@ class Index extends App.ControllerSubContent
) )
App.Config.set('LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index, permission: ['user_preferences.linked_accounts'] }, 'NavBarProfile') App.Config.set('LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index, permission: ['user_preferences.linked_accounts'] }, 'NavBarProfile')
App.Config.set('auth_provider_all', {
facebook:
url: '/auth/facebook'
name: 'Facebook'
config: 'auth_facebook'
class: 'facebook'
twitter:
url: '/auth/twitter'
name: 'Twitter'
config: 'auth_twitter'
class: 'twitter'
linkedin:
url: '/auth/linkedin'
name: 'LinkedIn'
config: 'auth_linkedin'
class: 'linkedin'
github:
url: '/auth/github'
name: 'GitHub'
config: 'auth_github'
class: 'github'
gitlab:
url: '/auth/gitlab'
name: 'GitLab'
config: 'auth_gitlab'
class: 'gitlab'
microsoft_office365:
url: '/auth/microsoft_office365'
name: 'Office 365'
config: 'auth_microsoft_office365'
class: 'office365'
google_oauth2:
url: '/auth/google_oauth2'
name: 'Google'
config: 'auth_google_oauth2'
class: 'google'
oauth2:
url: '/auth/oauth2'
name: 'OAuth2'
config: 'auth_oauth2'
class: 'oauth2'
})

View file

@ -75,13 +75,14 @@ class Index extends App.ControllerSubContent
groups = [] groups = []
group_ids = @Session.get('group_ids') group_ids = @Session.get('group_ids')
if group_ids if group_ids
for group_id in group_ids for group_id, access of group_ids
group = App.Group.find(group_id) if _.contains(access, 'full')
groups.push group group = App.Group.find(group_id)
if !user_group_config groups.push group
if !config['group_ids'] if !user_group_config
config['group_ids'] = [] if !config['group_ids']
config['group_ids'].push group_id.toString() config['group_ids'] = []
config['group_ids'].push group_id.toString()
for sound in @sounds for sound in @sounds
sound.selected = sound.file is App.OnlineNotification.soundFile() ? true : false sound.selected = sound.file is App.OnlineNotification.soundFile() ? true : false
@ -90,7 +91,7 @@ class Index extends App.ControllerSubContent
groups: groups groups: groups
config: config config: config
sounds: @sounds sounds: @sounds
notification_sound_enabled: App.OnlineNotification.soundEnabled() notificationSoundEnabled: App.OnlineNotification.soundEnabled()
update: (e) => update: (e) =>

View file

@ -0,0 +1,164 @@
class Index extends App.ControllerSubContent
requiredPermission: 'user_preferences.out_of_office+ticket.agent'
header: 'Out of Office'
events:
'submit form': 'submit'
'click .js-disabled': 'disable'
'click .js-enable': 'enable'
constructor: ->
super
@render()
render: =>
user = @Session.get()
if !@localData
@localData =
out_of_office: user.out_of_office
out_of_office_start_at: user.out_of_office_start_at
out_of_office_end_at: user.out_of_office_end_at
out_of_office_replacement_id: user.out_of_office_replacement_id
out_of_office_replacement_id_completion: user.preferences.out_of_office_replacement_id_completion
out_of_office_text: user.preferences.out_of_office_text
form = $(App.view('profile/out_of_office')(
user: user
localData: @localData
placeholder: App.User.outOfOfficeTextPlaceholder()
))
dateStart = new App.ControllerForm(
model:
configure_attributes:
[
name: 'out_of_office_start_at'
display: ''
tag: 'date'
past: false
future: true
null: false
]
noFieldset: true
params: @localData
)
form.find('.js-startDate').html(dateStart.form)
dateEnd = new App.ControllerForm(
model:
configure_attributes:
[
name: 'out_of_office_end_at'
display: ''
tag: 'date'
past: false
future: true
null: false
]
noFieldset: true
params: @localData
)
form.find('.js-endDate').html(dateEnd.form)
agentList = new App.ControllerForm(
model:
configure_attributes:
[
name: 'out_of_office_replacement_id'
display: ''
relation: 'User'
tag: 'user_autocompletion'
autocapitalize: false
multiple: false
limit: 30
minLengt: 2
placeholder: 'Enter Person or Organization/Company'
null: false
translate: false
disableCreateObject: true
value: @localData
]
noFieldset: true
params: @localData
)
form.find('.js-recipientDropdown').html(agentList.form)
if @localData.out_of_office is true
form.find('.js-disabled').removeClass('is-disabled')
#form.find('.js-enable').addClass('is-disabled')
else
form.find('.js-disabled').addClass('is-disabled')
#form.find('.js-enable').removeClass('is-disabled')
@html(form)
enable: (e) =>
e.preventDefault()
params = @formParam(e.target)
params.out_of_office = true
@store(e, params)
disable: (e) =>
e.preventDefault()
params = @formParam(e.target)
params.out_of_office = false
@store(e, params)
submit: (e, params) =>
e.preventDefault()
params = @formParam(e.target)
@store(e, params)
store: (e, params) =>
@formDisable(e)
for key, value of params
@localData[key] = value
App.Ajax.request(
id: 'user_out_of_office'
type: 'PUT'
url: "#{@apiPath}/users/out_of_office"
data: JSON.stringify(params)
processData: true
success: @success
error: @error
)
success: (data) =>
if data.message is 'ok'
@render()
@notify(
type: 'success'
msg: App.i18n.translateContent('Successfully!')
timeout: 1000
)
else
if data.notice
@notify
type: 'error'
msg: App.i18n.translateContent(data.notice[0], data.notice[1])
removeAll: true
else
@notify
type: 'error'
msg: 'Please contact your administrator.'
removeAll: true
@formEnable( @$('form') )
error: (xhr, status, error) =>
@formEnable( @$('form') )
# do not close window if request is aborted
return if status is 'abort'
data = JSON.parse(xhr.responseText)
# show error message
if xhr.status is 401 || error is 'Unauthorized'
message = '» ' + App.i18n.translateInline('Unauthorized') + ' «'
else if xhr.status is 404 || error is 'Not Found'
message = '» ' + App.i18n.translateInline('Not Found') + ' «'
else if data.error
message = App.i18n.translateInline(data.error)
else
message = '» ' + App.i18n.translateInline('Error') + ' «'
@notify
type: 'error'
msg: App.i18n.translateContent(message)
removeAll: true
App.Config.set('OutOfOffice', { prio: 2800, name: 'Out of Office', parent: '#profile', target: '#profile/out_of_office', permission: ['user_preferences.out_of_office+ticket.agent'], controller: Index }, 'NavBarProfile')

View file

@ -2,7 +2,7 @@ class App.SettingsAreaProxy extends App.Controller
events: events:
'submit form': 'update' 'submit form': 'update'
'click .js-submit': 'update' 'click .js-submit': 'update'
'click .js-test': 'test2' 'click .js-test': 'testConnection'
constructor: -> constructor: ->
super super
@ -14,20 +14,21 @@ class App.SettingsAreaProxy extends App.Controller
proxy: App.Setting.get('proxy') proxy: App.Setting.get('proxy')
proxy_username: App.Setting.get('proxy_username') proxy_username: App.Setting.get('proxy_username')
proxy_password: App.Setting.get('proxy_password') proxy_password: App.Setting.get('proxy_password')
proxy_no: App.Setting.get('proxy_no')
) )
update: (e) => update: (e) =>
e.preventDefault() e.preventDefault()
@formDisable(e) @formDisable(e)
params = @formParam(e) params = @formParam(e)
console.log('params', params)
App.Setting.set('proxy', params.proxy) App.Setting.set('proxy', params.proxy)
App.Setting.set('proxy_username', params.proxy_username) App.Setting.set('proxy_username', params.proxy_username)
App.Setting.set('proxy_password', params.proxy_password) App.Setting.set('proxy_password', params.proxy_password)
App.Setting.set('proxy_no', params.proxy_no)
@formEnable(e) @formEnable(e)
@render() @render()
test2: (e) => testConnection: (e) =>
e.preventDefault() e.preventDefault()
params = @formParam(e) params = @formParam(e)
@ajax( @ajax(

View file

@ -15,7 +15,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
attribute: attribute attribute: attribute
params: params params: params
)) ))
@[localParams.data_type](element, localParams, params) @[localParams.data_type](element, localParams, params, attribute)
localItem.find('.js-dataMap').html(element) localItem.find('.js-dataMap').html(element)
localItem.find('.js-dataScreens').html(@dataScreens(attribute, localParams, params)) localItem.find('.js-dataScreens').html(@dataScreens(attribute, localParams, params))
@ -24,6 +24,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
date: 'Date' date: 'Date'
input: 'Text' input: 'Text'
select: 'Select' select: 'Select'
tree_select: 'Tree Select'
boolean: 'Boolean' boolean: 'Boolean'
integer: 'Integer' integer: 'Integer'
@ -81,9 +82,9 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
view: view:
shown: true shown: true
invite_customer: invite_customer:
show: false shown: false
required: false required: false
'admin.group': 'admin.user':
create: create:
shown: true shown: true
required: false required: false
@ -93,10 +94,10 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
view: view:
shown: true shown: true
invite_agent: invite_agent:
show: false shown: false
required: false required: false
invite_customer: invite_customer:
show: false shown: false
required: false required: false
Organization: Organization:
'ticket.customer': 'ticket.customer':
@ -111,7 +112,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
required: false required: false
view: view:
shown: true shown: true
'admin.group': 'admin.organization':
create: create:
shown: true shown: true
required: false required: false
@ -308,6 +309,69 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
lastSelected = value lastSelected = value
) )
@buildRow: (element, child, level = 0, parentElement) ->
newRow = element.find('.js-template').clone().removeClass('js-template')
newRow.find('.js-key').attr('level', level)
newRow.find('.js-key').val(child.name)
newRow.find('td').first().css('padding-left', "#{(level * 20) + 10}px")
if level is 5
newRow.find('.js-addChild').addClass('hide')
if parentElement
parentElement.after(newRow)
return
element.find('.js-treeTable').append(newRow)
if child.children
for subChild in child.children
@buildRow(element, subChild, level + 1)
@tree_select: (item, localParams, params, attribute) ->
params.data_option ||= {}
params.data_option.options ||= []
if _.isEmpty(params.data_option.options)
@buildRow(item, {})
else
for child in params.data_option.options
@buildRow(item, child)
item.on('click', '.js-addRow', (e) =>
e.stopPropagation()
e.preventDefault()
addRow = $(e.currentTarget).closest('tr')
level = parseInt(addRow.find('.js-key').attr('level'))
@buildRow(item, {}, level, addRow)
)
item.on('click', '.js-addChild', (e) =>
e.stopPropagation()
e.preventDefault()
addRow = $(e.currentTarget).closest('tr')
level = parseInt(addRow.find('.js-key').attr('level')) + 1
@buildRow(item, {}, level, addRow)
)
item.on('click', '.js-remove', (e) ->
e.stopPropagation()
e.preventDefault()
e.stopPro
element = $(e.target).closest('tr')
level = parseInt(element.find('.js-key').attr('level'))
subElements = 0
nextElement = element
elementsToDelete = [element]
loop
nextElement = nextElement.next()
break if !nextElement.get(0)
nextLevel = parseInt(nextElement.find('.js-key').attr('level'))
break if nextLevel <= level
subElements += 1
elementsToDelete.push nextElement
return if subElements isnt 0 && !confirm("Delete #{subElements} sub elements?")
for element in elementsToDelete
element.remove()
)
@boolean: (item, localParams, params) -> @boolean: (item, localParams, params) ->
lastSelected = undefined lastSelected = undefined
item.on('click', '.js-selected', (e) -> item.on('click', '.js-selected', (e) ->

View file

@ -4,10 +4,25 @@ class App.UiElement.permission extends App.UiElement.ApplicationUiElement
permissions = App.Permission.search(sortBy: 'name') permissions = App.Permission.search(sortBy: 'name')
# get selectable groups and selected groups
groups = []
groupsSelected = {}
groupsRaw = App.Group.search(sortBy: 'name')
for group in groupsRaw
if group.active
groups.push group
if params.group_ids
for group_id in params.group_ids
if group_id.toString() is group.id.toString()
groupsSelected[group.id] = true
item = $( App.view('generic/permission')( item = $( App.view('generic/permission')(
attribute: attribute attribute: attribute
params: params params: params
permissions: permissions permissions: permissions
groups: groups
groupsSelected: groupsSelected
groupAccesses: App.Group.accesses()
) ) ) )
# show/hide trees # show/hide trees
@ -37,4 +52,4 @@ class App.UiElement.permission extends App.UiElement.ApplicationUiElement
) )
item item

View file

@ -20,7 +20,7 @@ class App.UiElement.postmaster_set
name: 'Customer' name: 'Customer'
relation: 'User' relation: 'User'
tag: 'user_autocompletion' tag: 'user_autocompletion'
disableCreateUser: true disableCreateObject: true
} }
{ {
value: 'group_id' value: 'group_id'
@ -32,7 +32,7 @@ class App.UiElement.postmaster_set
name: 'Owner' name: 'Owner'
relation: 'User' relation: 'User'
tag: 'user_autocompletion' tag: 'user_autocompletion'
disableCreateUser: true disableCreateObject: true
} }
] ]
article: article:

View file

@ -9,24 +9,24 @@ class App.UiElement.searchable_select extends App.UiElement.ApplicationUiElement
attribute.multiple = '' attribute.multiple = ''
# build options list based on config # build options list based on config
@getConfigOptionList( attribute, params ) @getConfigOptionList(attribute, params)
# build options list based on relation # build options list based on relation
@getRelationOptionList( attribute, params ) @getRelationOptionList(attribute, params)
# add null selection if needed # add null selection if needed
@addNullOption( attribute, params ) @addNullOption(attribute, params)
# sort attribute.options # sort attribute.options
@sortOptions( attribute, params ) @sortOptions(attribute, params)
# finde selected/checked item of list # finde selected/checked item of list
@selectedOptions( attribute, params ) @selectedOptions(attribute, params)
# disable item of list # disable item of list
@disabledOptions( attribute, params ) @disabledOptions(attribute, params)
# filter attributes # filter attributes
@filterOption( attribute, params ) @filterOption(attribute, params)
new App.SearchableSelect( attribute: attribute ).element() new App.SearchableSelect(attribute: attribute).element()

View file

@ -33,14 +33,15 @@ class App.UiElement.ticket_perform_action
elements["#{groupKey}.#{config.name}"] = config elements["#{groupKey}.#{config.name}"] = config
# add ticket deletion action # add ticket deletion action
elements['ticket.action'] = if attribute.ticket_delete
name: 'action' elements['ticket.action'] =
display: 'Action' name: 'action'
tag: 'select' display: 'Action'
null: false tag: 'select'
translate: true null: false
options: translate: true
delete: 'delete' options:
delete: 'Delete'
[defaults, groups, elements] [defaults, groups, elements]

View file

@ -156,7 +156,7 @@ class App.UiElement.ticket_selector
elementRow = $(e.target).closest('.js-filterElement') elementRow = $(e.target).closest('.js-filterElement')
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value') groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
return if !groupAndAttribute return if !groupAndAttribute
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute, false) @buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
) )
# bind for preview # bind for preview
@ -244,9 +244,9 @@ class App.UiElement.ticket_selector
if groupAndAttribute if groupAndAttribute
elementRow.find('.js-attributeSelector select').val(groupAndAttribute) elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
@buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute, true) @buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute, buildValue) -> @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value') currentOperator = elementRow.find('.js-operator option:selected').attr('value')
name = "#{attribute.name}::#{groupAndAttribute}::operator" name = "#{attribute.name}::#{groupAndAttribute}::operator"
@ -284,9 +284,9 @@ class App.UiElement.ticket_selector
elementRow.find('.js-operator select').replaceWith(selection) elementRow.find('.js-operator select').replaceWith(selection)
@buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute, buildValue) @buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig, buildValue = true) -> @buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
currentOperator = elementRow.find('.js-operator option:selected').attr('value') currentOperator = elementRow.find('.js-operator option:selected').attr('value')
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value') currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
@ -318,7 +318,6 @@ class App.UiElement.ticket_selector
if !preCondition if !preCondition
elementRow.find('.js-preCondition select').html('') elementRow.find('.js-preCondition select').html('')
elementRow.find('.js-preCondition').addClass('hide') elementRow.find('.js-preCondition').addClass('hide')
return if !buildValue
toggleValue() toggleValue()
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
return return
@ -351,7 +350,6 @@ class App.UiElement.ticket_selector
toggleValue() toggleValue()
) )
return if !buildValue
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute) @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
toggleValue() toggleValue()

View file

@ -0,0 +1,41 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.tree_select extends App.UiElement.ApplicationUiElement
@optionsSelect: (children, value) ->
return if !children
for child in children
if child.value is value
child.selected = true
if child.children
@optionsSelect(child.children, value)
@render: (attribute, params) ->
# set multiple option
if attribute.multiple
attribute.multiple = 'multiple'
else
attribute.multiple = ''
# build options list based on config
@getConfigOptionList(attribute, params)
# build options list based on relation
@getRelationOptionList(attribute, params)
# add null selection if needed
@addNullOption(attribute, params)
# sort attribute.options
@sortOptions(attribute, params)
# finde selected/checked item of list
if attribute.options
@optionsSelect(attribute.options, attribute.value)
# disable item of list
@disabledOptions(attribute, params)
# filter attributes
@filterOption(attribute, params)
new App.SearchableSelect(attribute: attribute).element()

View file

@ -2,5 +2,5 @@
class App.UiElement.user_autocompletion_search class App.UiElement.user_autocompletion_search
@render: (attributeOrig, params = {}) -> @render: (attributeOrig, params = {}) ->
attribute = _.clone(attributeOrig) attribute = _.clone(attributeOrig)
attribute.disableCreateUser = true attribute.disableCreateObject = true
new App.UserOrganizationAutocompletion(attribute: attribute, params: params).element() new App.UserOrganizationAutocompletion(attribute: attribute, params: params).element()

View file

@ -72,6 +72,7 @@ class App.UiElement.user_permission
rolesSelected: rolesSelected rolesSelected: rolesSelected
groupsSelected: groupsSelected groupsSelected: groupsSelected
hideGroups: hideGroups hideGroups: hideGroups
groupAccesses: App.Group.accesses()
) ) ) )
# if customer, remove admin and agent # if customer, remove admin and agent
@ -105,7 +106,7 @@ class App.UiElement.user_permission
# select groups if only one is available # select groups if only one is available
if hideGroups if hideGroups
item.find('.js-groupList [name=group_ids]').prop('checked', false) item.find('.js-groupList .js-groupListItem[value=full]').prop('checked', false)
return return
# if role with groups plugin is selected, show group selection # if role with groups plugin is selected, show group selection
@ -114,7 +115,7 @@ class App.UiElement.user_permission
# select groups if only one is available # select groups if only one is available
if hideGroups if hideGroups
item.find('.js-groupList [name=group_ids]').prop('checked', true) item.find('.js-groupList .js-groupListItem[value=full]').prop('checked', true)
for trigger in triggers for trigger in triggers
trigger.trigger('change') trigger.trigger('change')

View file

@ -9,6 +9,7 @@ class App.TicketCreate extends App.Controller
constructor: (params) -> constructor: (params) ->
super super
@sidebarState = {}
# define default type # define default type
@default_type = 'phone-in' @default_type = 'phone-in'
@ -91,6 +92,8 @@ class App.TicketCreate extends App.Controller
else else
@$('[name="cc"]').closest('.form-group').addClass('hide') @$('[name="cc"]').closest('.form-group').addClass('hide')
App.TaskManager.touch(@task_key)
meta: => meta: =>
text = '' text = ''
if @articleAttributes if @articleAttributes
@ -99,10 +102,10 @@ class App.TicketCreate extends App.Controller
if title if title
text = "#{text}: #{title}" text = "#{text}: #{title}"
meta = meta =
url: @url() url: @url()
head: text head: text
title: text title: text
id: @id id: @id
iconClass: 'pen' iconClass: 'pen'
url: => url: =>
@ -228,7 +231,7 @@ class App.TicketCreate extends App.Controller
type = @$('[name="formSenderType"]').val() type = @$('[name="formSenderType"]').val()
if signature isnt undefined && signature.body && type is 'email-out' if signature isnt undefined && signature.body && type is 'email-out'
signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get() }) signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), config: App.Config.all() })
body = @$('[data-name=body]') body = @$('[data-name=body]')
if App.Utils.signatureCheck(body.html() || '', signatureFinished) if App.Utils.signatureCheck(body.html() || '', signatureFinished)
@ -330,27 +333,47 @@ class App.TicketCreate extends App.Controller
# show text module UI # show text module UI
@textModule = new App.WidgetTextModule( @textModule = new App.WidgetTextModule(
el: @$('[data-name="body"]').parent() el: @$('[data-name="body"]').parent()
) data:
config: App.Config.all()
new Sidebar( user: App.Session.get()
el: @sidebar
params: @formDefault
textModule: @textModule
) )
$('#tags').tokenfield() $('#tags').tokenfield()
@sidebarWidget = new App.TicketCreateSidebar(
el: @sidebar
params: @formDefault
sidebarState: @sidebarState
task_key: @task_key
query: @query
)
if @formDefault.customer_id
callback = (customer) =>
@localUserInfoCallback(@formDefault, customer)
App.User.full(@formDefault.customer_id, callback)
# update taskbar with new meta data # update taskbar with new meta data
App.TaskManager.touch(@task_key) App.TaskManager.touch(@task_key)
localUserInfo: (e) => localUserInfo: (e) =>
return if !@sidebarWidget
params = App.ControllerForm.params($(e.target).closest('form')) params = App.ControllerForm.params($(e.target).closest('form'))
new Sidebar( if params.customer_id
el: @sidebar callback = (customer) =>
params: params @localUserInfoCallback(params, customer)
textModule: @textModule App.User.full(params.customer_id, callback)
return
@localUserInfoCallback(params)
localUserInfoCallback: (params, customer = {}) =>
@sidebarWidget.render(params)
@textModule.reload(
config: App.Config.all()
user: App.Session.get()
ticket:
customer: customer
) )
cancel: (e) -> cancel: (e) ->
@ -475,11 +498,16 @@ class App.TicketCreate extends App.Controller
# scroll to top # scroll to top
ui.scrollTo() ui.scrollTo()
# add sidebar params
if ui.sidebarWidget
ui.sidebarWidget.commit(ticket_id: @id)
# access to group # access to group
group_ids = _.map(App.Session.get('group_ids'), (id) -> id.toString()) for group_id, access of App.Session.get('group_ids')
if group_ids && _.contains(group_ids, @group_id.toString()) if @group_id.toString() is group_id.toString()
ui.navigate "#ticket/zoom/#{@id}" if _.contains(access, 'read') || _.contains(access, 'full')
return ui.navigate "#ticket/zoom/#{@id}"
return
# if not, show start screen # if not, show start screen
ui.navigate '#' ui.navigate '#'
@ -494,114 +522,6 @@ class App.TicketCreate extends App.Controller
) )
) )
class Sidebar extends App.Controller
constructor: ->
super
# load user
if @params['customer_id']
App.User.full(@params['customer_id'], @render)
return
# render ui
@render()
render: (user) =>
items = []
if user
showCustomer = (el) =>
# update text module UI
if @textModule
@textModule.reload(
ticket:
customer: user
user: App.Session.get()
)
new App.WidgetUser(
el: el
user_id: user.id
)
editCustomer = (e, el) =>
new App.ControllerGenericEdit(
id: @params.customer_id
genericObject: 'User'
screen: 'edit'
pageData:
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
)
items.push {
head: 'Customer'
name: 'customer'
icon: 'person'
actions: [
{
title: 'Edit Customer'
name: 'Edit Customer'
class: 'glyphicon glyphicon-edit'
callback: editCustomer
},
]
callback: showCustomer
}
if user.organization_id
editOrganization = (e, el) =>
new App.ControllerGenericEdit(
id: user.organization_id
genericObject: 'Organization'
pageData:
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
)
showOrganization = (el) ->
new App.WidgetOrganization(
el: el
organization_id: user.organization_id
)
items.push {
head: 'Organization'
name: 'organization'
icon: 'group'
actions: [
{
title: 'Edit Organization'
name: 'Edit Organization'
class: 'glyphicon glyphicon-edit'
callback: editOrganization
},
]
callback: showOrganization
}
showTemplates = (el) ->
# show template UI
new App.WidgetTemplate(
el: el
#template_id: template['id']
)
items.push {
head: 'Templates'
name: 'template'
icon: 'templates'
callback: showTemplates
}
new App.Sidebar(
el: @el
items: items
)
class Router extends App.ControllerPermanent class Router extends App.ControllerPermanent
requiredPermission: 'ticket.agent' requiredPermission: 'ticket.agent'
constructor: (params) -> constructor: (params) ->
@ -617,6 +537,9 @@ class Router extends App.ControllerPermanent
if params.customer_id if params.customer_id
split = "/customer/#{params.customer_id}" split = "/customer/#{params.customer_id}"
if params.query
split = "/query/#{params.query}"
id = Math.floor( Math.random() * 99999 ) id = Math.floor( Math.random() * 99999 )
@navigate "#ticket/create/id/#{id}#{split}" @navigate "#ticket/create/id/#{id}#{split}"
return return
@ -627,6 +550,7 @@ class Router extends App.ControllerPermanent
article_id: params.article_id article_id: params.article_id
type: params.type type: params.type
customer_id: params.customer_id customer_id: params.customer_id
query: params.query
id: params.id id: params.id
App.TaskManager.execute( App.TaskManager.execute(
@ -642,6 +566,7 @@ App.Config.set('ticket/create/', Router, 'Routes')
App.Config.set('ticket/create/id/:id', Router, 'Routes') App.Config.set('ticket/create/id/:id', Router, 'Routes')
App.Config.set('ticket/create/customer/:customer_id', Router, 'Routes') App.Config.set('ticket/create/customer/:customer_id', Router, 'Routes')
App.Config.set('ticket/create/id/:id/customer/:customer_id', Router, 'Routes') App.Config.set('ticket/create/id/:id/customer/:customer_id', Router, 'Routes')
App.Config.set('ticket/create/id/:id/query/:query', Router, 'Routes')
# split ticket # split ticket
App.Config.set('ticket/create/:ticket_id/:article_id', Router, 'Routes') App.Config.set('ticket/create/:ticket_id/:article_id', Router, 'Routes')

View file

@ -0,0 +1,43 @@
class App.TicketCreateSidebar extends App.Controller
constructor: ->
super
@render()
reload: (args) =>
for key, backend of @sidebarBackends
if backend && backend.reload
backend.reload(args)
commit: (args) =>
for key, backend of @sidebarBackends
if backend && backend.commit
backend.commit(args)
render: (params) =>
if params
@params = params
@sidebarBackends ||= {}
@sidebarItems = []
sidebarBackends = App.Config.get('TicketCreateSidebar')
keys = _.keys(sidebarBackends).sort()
for key in keys
if !@sidebarBackends[key] || !@sidebarBackends[key].reload
@sidebarBackends[key] = new sidebarBackends[key](
params: @params
query: @query
taskGet: @taskGet
)
else
@sidebarBackends[key].reload(
params: @params
query: @query
)
item = @sidebarBackends[key].sidebarItem()
if item
@sidebarItems.push item
new App.Sidebar(
el: @el
sidebarState: @sidebarState
items: @sidebarItems
)

View file

@ -0,0 +1,38 @@
class SidebarCustomer extends App.Controller
sidebarItem: =>
return if !@permissionCheck('ticket.agent')
return if !@params.customer_id
{
head: 'Customer'
name: 'customer'
icon: 'person'
actions: [
{
title: 'Edit Customer'
name: 'customer-edit'
callback: @editCustomer
},
]
callback: @showCustomer
}
showCustomer: (el) =>
@el = el
new App.WidgetUser(
el: @el
user_id: @params.customer_id
)
editCustomer: =>
new App.ControllerGenericEdit(
id: @params.customer_id
genericObject: 'User'
screen: 'edit'
pageData:
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
)
App.Config.set('200-Customer', SidebarCustomer, 'TicketCreateSidebar')

View file

@ -0,0 +1,41 @@
class SidebarOrganization extends App.Controller
sidebarItem: =>
return if !@permissionCheck('ticket.agent')
return if !@params.customer_id
return if !App.User.exists(@params.customer_id)
customer = App.User.find(@params.customer_id)
@organization_id = customer.organization_id
return if !@organization_id
{
head: 'Organization'
name: 'organization'
icon: 'group'
actions: [
{
title: 'Edit Organization'
name: 'organization-edit'
callback: @editOrganization
},
]
callback: @showOrganization
}
showOrganization: (el) =>
@el = el
new App.WidgetOrganization(
el: @el
organization_id: @organization_id
)
editOrganization: =>
new App.ControllerGenericEdit(
id: @organization_id,
genericObject: 'Organization'
pageData:
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
)
App.Config.set('300-Organization', SidebarOrganization, 'TicketCreateSidebar')

View file

@ -0,0 +1,21 @@
class SidebarTemplate extends App.Controller
sidebarItem: =>
return if !@permissionCheck('ticket.agent')
{
head: 'Templates'
name: 'template'
icon: 'templates'
actions: []
callback: @showTemplates
}
showTemplates: (el) =>
@el = el
# show template UI
new App.WidgetTemplate(
el: el
#template_id: template['id']
)
App.Config.set('100-Template', SidebarTemplate, 'TicketCreateSidebar')

View file

@ -98,9 +98,13 @@ class App.TicketMerge extends App.ControllerModal
type: 'error' type: 'error'
msg: App.i18n.translateContent(data['message']) msg: App.i18n.translateContent(data['message'])
timeout: 6000 timeout: 6000
@formEnable(e) @formEnable(e)
error: => error: (data) =>
details = data.responseJSON || {}
@notify
type: 'error'
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to merge!')
timeout: 6000
@formEnable(e) @formEnable(e)
) )

View file

@ -462,6 +462,14 @@ class ChatWindow extends App.Controller
) )
) )
# show text module UI
new App.WidgetTextModule(
el: @input
data:
user: App.Session.get()
config: App.Config.all()
)
focus: => focus: =>
@input.focus() @input.focus()
@ -473,7 +481,7 @@ class ChatWindow extends App.Controller
if event.data and event.data.callback if event.data and event.data.callback
event.data.callback() event.data.callback()
@$('.js-customerChatInput').ce({ @input.ce({
mode: 'richtext' mode: 'richtext'
multiline: true multiline: true
maxlength: 40000 maxlength: 40000
@ -522,7 +530,7 @@ class ChatWindow extends App.Controller
switch event.keyCode switch event.keyCode
when TABKEY when TABKEY
allChatInputs = $('.js-customerChatInput').not('[disabled="disabled"]') allChatInputs = @input.not('[disabled="disabled"]')
chatCount = allChatInputs.size() chatCount = allChatInputs.size()
index = allChatInputs.index(@input) index = allChatInputs.index(@input)
@ -542,7 +550,7 @@ class ChatWindow extends App.Controller
allChatInputs.eq(chatCount-1).focus() allChatInputs.eq(chatCount-1).focus()
when ENTERKEY when ENTERKEY
if !event.shiftKey if !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey
event.preventDefault() event.preventDefault()
@sendMessage() @sendMessage()
@ -587,7 +595,7 @@ class ChatWindow extends App.Controller
@sounds.message.play() @sounds.message.play()
@notifyDesktop( @notifyDesktop(
title: @name title: @name
body: message body: App.Utils.html2text(message)
url: '#customer_chat' url: '#customer_chat'
callback: => callback: =>
App.Event.trigger('chat_focus', { session_id: @session.session_id }) App.Event.trigger('chat_focus', { session_id: @session.session_id })

View file

@ -450,8 +450,8 @@ class EmailNotification extends App.WizardFullScreen
if adapter is 'smtp' if adapter is 'smtp'
configureAttributesOutbound = [ configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password' }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off' },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
] ]
@form = new App.ControllerForm( @form = new App.ControllerForm(
@ -671,20 +671,24 @@ class ChannelEmail extends App.WizardFullScreen
# inbound # inbound
configureAttributesInbound = [ configureAttributesInbound = [
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', single: true },
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' },
{ name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
] ]
showHideFolder = (params, attribute, attributes, classname, form, ui) -> showHideFolder = (params, attribute, attributes, classname, form, ui) ->
return if !params return if !params
if params.adapter is 'imap' if params.adapter is 'imap'
ui.show('options::folder') ui.show('options::folder')
ui.show('options::keep_on_server')
return return
ui.hide('options::folder') ui.hide('options::folder')
ui.hide('options::keep_on_server')
handlePort = (params, attribute, attributes, classname, form, ui) -> handlePort = (params, attribute, attributes, classname, form, ui) ->
return if !params return if !params
@ -700,7 +704,7 @@ class ChannelEmail extends App.WizardFullScreen
return return
new App.ControllerForm( new App.ControllerForm(
el: @$('.base-inbound-settings'), el: @$('.base-inbound-settings')
model: model:
configure_attributes: configureAttributesInbound configure_attributes: configureAttributesInbound
className: '' className: ''
@ -716,8 +720,10 @@ class ChannelEmail extends App.WizardFullScreen
# fill user / password based on intro info # fill user / password based on intro info
channel_used = { options: {} } channel_used = { options: {} }
if @account['meta'] if @account['meta']
channel_used['options']['user'] = @account['meta']['email'] channel_used['options']['user'] = @account['meta']['email']
channel_used['options']['password'] = @account['meta']['password'] channel_used['options']['password'] = @account['meta']['password']
channel_used['options']['folder'] = @account['meta']['folder']
channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server']
# show used backend # show used backend
@$('.base-outbound-settings').html('') @$('.base-outbound-settings').html('')
@ -725,8 +731,8 @@ class ChannelEmail extends App.WizardFullScreen
if adapter is 'smtp' if adapter is 'smtp'
configureAttributesOutbound = [ configureAttributesOutbound = [
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
] ]
@form = new App.ControllerForm( @form = new App.ControllerForm(
@ -745,7 +751,7 @@ class ChannelEmail extends App.WizardFullScreen
@account.meta = params @account.meta = params
@disable(e) @disable(e)
@$('.js-probe .js-email').text( params.email ) @$('.js-probe .js-email').text(params.email)
@showSlide('js-probe') @showSlide('js-probe')
@ajax( @ajax(
@ -760,7 +766,7 @@ class ChannelEmail extends App.WizardFullScreen
for key, value of data.setting for key, value of data.setting
@account[key] = value @account[key] = value
if data.content_messages && data.content_messages > 0 if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
@$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-message').html(message)
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro') @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
@ -809,7 +815,7 @@ class ChannelEmail extends App.WizardFullScreen
# remember account settings # remember account settings
@account.inbound = params @account.inbound = params
if data.content_messages && data.content_messages > 0 if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
message = App.i18n.translateContent('We have already found %s emails in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) message = App.i18n.translateContent('We have already found %s emails in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
@$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-message').html(message)
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound') @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')

View file

@ -0,0 +1,100 @@
class App.IdoitObjectSelector extends App.ControllerModal
buttonClose: true
buttonCancel: true
buttonSubmit: true
head: 'i-doit'
content: ->
@ajax(
id: 'idoit-object-selector'
type: 'POST'
url: "#{@apiPath}/integration/idoit"
data: JSON.stringify(method: 'cmdb.object_types')
success: (data, status, xhr) =>
if data.result is 'failed'
@contentInline = data.message
@render()
return
result = _.sortBy(data.response.result, 'title')
@contentInline = $(App.view('integration/idoit_object_selector')())
@contentInline.find('.js-typeSelect').html(@renderTypeSelector(result))
@contentInline.filter('.js-search').on('change', 'select, input', (e) =>
params = @formParam(e.target)
@search(params)
)
@contentInline.filter('.js-search').on('keyup', 'input', (e) =>
params = @formParam(e.target)
@search(params)
)
@render()
@$('.js-input').focus()
error: (xhr, status, error) =>
# do not close window if request is aborted
return if status is 'abort'
# show error message
@contentInline = 'Unable to load content'
@render()
)
''
search: (filter) =>
if _.isEmpty(filter.title)
delete filter.title
else
filter.title = "%#{filter.title}%"
@ajax(
id: 'idoit-object-selector'
type: 'POST'
url: "#{@apiPath}/integration/idoit"
data: JSON.stringify(method: 'cmdb.objects', filter: filter)
success: (data, status, xhr) =>
@renderResult(data.response.result)
error: (xhr, status, error) =>
# do not close window if request is aborted
return if status is 'abort'
# show error message
@contentInline = 'Unable to load content'
@render()
)
renderResult: (items) =>
table = App.view('integration/idoit_object_result')(
items: items
)
@el.find('.js-result').html(table)
renderTypeSelector: (result) ->
options = {}
for item in result
options[item.id] = item.title
return App.UiElement.searchable_select.render(
name: 'type'
multiple: false
limit: 100
null: false
nulloption: false
options: options
)
onSubmit: (e) =>
form = @el.find('.js-result')
params = @formParam(form)
return if _.isEmpty(params.object_id)
if _.isArray(params.object_id)
object_ids = params.object_id
else
object_ids = [params.object_id]
@formDisable(form)
@callback(object_ids, @)

View file

@ -10,6 +10,7 @@ class Index extends App.ControllerContent
'.zendesk-api-token-error': 'apiTokenErrorMessage' '.zendesk-api-token-error': 'apiTokenErrorMessage'
'#zendesk-email': 'zendeskEmail' '#zendesk-email': 'zendeskEmail'
'#zendesk-api-token': 'zendeskApiToken' '#zendesk-api-token': 'zendeskApiToken'
'.js-ticket-count-info': 'ticketCountInfo'
updateMigrationDisplayLoop: 0 updateMigrationDisplayLoop: 0
events: events:
@ -116,7 +117,8 @@ class Index extends App.ControllerContent
showCredentials: (e) => showCredentials: (e) =>
e.preventDefault() e.preventDefault()
@urlStatus.attr('data-state', '') @urlStatus.attr('data-state', '')
@zendeskUrlApiToken.attr('href', @zendeskUrl.val() + 'agent/admin/api') url = @zendeskUrl.val() + '/agent/admin/api'
@zendeskUrlApiToken.attr('href', url.replace(/([^:])\/\/+/g, '$1/'))
@zendeskUrlApiToken.val('HERE') @zendeskUrlApiToken.val('HERE')
@$('[data-slide=zendesk-url]').toggleClass('hide') @$('[data-slide=zendesk-url]').toggleClass('hide')
@$('[data-slide=zendesk-credentials]').toggleClass('hide') @$('[data-slide=zendesk-credentials]').toggleClass('hide')
@ -171,6 +173,10 @@ class Index extends App.ControllerContent
for key, item of data.data for key, item of data.data
if item.done > item.total if item.done > item.total
item.done = item.total item.done = item.total
if key == 'Ticket' && item.total >= 1000
@ticketCountInfo.removeClass('hide')
element = @$('.js-' + key.toLowerCase() ) element = @$('.js-' + key.toLowerCase() )
element.find('.js-done').text(item.done) element.find('.js-done').text(item.done)
element.find('.js-total').text(item.total) element.find('.js-total').text(item.total)

View file

@ -1499,7 +1499,7 @@ class InputsRef extends App.ControllerContent
null: false null: false
relation: 'User' relation: 'User'
autocapitalize: false autocapitalize: false
disableCreateUser: true disableCreateObject: true
multiple: true multiple: true
@$('.userOrganizationAutocompletePlaceholder').replaceWith( userOrganizationAutocomplete.element() ) @$('.userOrganizationAutocompletePlaceholder').replaceWith( userOrganizationAutocomplete.element() )

View file

@ -38,50 +38,7 @@ class Index extends App.ControllerContent
) )
render: (data = {}) -> render: (data = {}) ->
auth_provider_all = { auth_provider_all = App.Config.get('auth_provider_all')
facebook: {
url: '/auth/facebook',
name: 'Facebook',
config: 'auth_facebook',
class: 'facebook'
},
twitter: {
url: '/auth/twitter'
name: 'Twitter'
config: 'auth_twitter'
class: 'twitter'
},
linkedin: {
url: '/auth/linkedin'
name: 'LinkedIn'
config: 'auth_linkedin'
class: 'linkedin'
},
github: {
url: '/auth/github'
name: 'GitHub'
config: 'auth_github'
class: 'github'
},
gitlab: {
url: '/auth/gitlab'
name: 'GitLab'
config: 'auth_gitlab'
class: 'gitlab'
},
google_oauth2: {
url: '/auth/google_oauth2'
name: 'Google'
config: 'auth_google_oauth2'
class: 'google'
},
oauth2: {
url: '/auth/oauth2'
name: 'OAuth2'
config: 'auth_oauth2'
class: 'oauth2'
},
}
auth_providers = [] auth_providers = []
for key, provider of auth_provider_all for key, provider of auth_provider_all
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true' if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'

View file

@ -1,4 +1,46 @@
# coffeelint: disable=duplicate_key # coffeelint: disable=duplicate_key
treeParams = (e, params) ->
tree = []
lastLevel = 0
lastLevels = []
valueLevels = []
$(e.target).closest('.modal').find('.js-treeTable .js-key').each( ->
$element = $(@)
level = parseInt($element.attr('level'))
name = $element.val()
item =
name: name
if level is 0
tree.push item
else if lastLevels[level-1]
lastLevels[level-1].children ||= []
lastLevels[level-1].children.push item
else
console.log('ERROR', item)
if level is 0
valueLevels = []
else if lastLevel is level
valueLevels.pop()
else if lastLevel > level
down = lastLevel - level
for count in [1..down]
valueLevels.pop()
if lastLevel <= level
valueLevels.push name
item.value = valueLevels.join('::')
lastLevels[level] = item
lastLevel = level
)
if tree[0]
if !params.data_option
params.data_option = {}
params.data_option.options = tree
params
class Index extends App.ControllerTabs class Index extends App.ControllerTabs
requiredPermission: 'admin.object' requiredPermission: 'admin.object'
constructor: -> constructor: ->
@ -135,6 +177,7 @@ class New extends App.ControllerGenericNew
onSubmit: (e) => onSubmit: (e) =>
params = @formParam(e.target) params = @formParam(e.target)
params = treeParams(e, params)
# show attributes for create_middle in two column style # show attributes for create_middle in two column style
if params.screens && params.screens.create_middle if params.screens && params.screens.create_middle
@ -184,6 +227,8 @@ class Edit extends App.ControllerGenericEdit
#if attribute.name is 'data_type' #if attribute.name is 'data_type'
# attribute.disabled = true # attribute.disabled = true
console.log('configure_attributes', configure_attributes)
@controller = new App.ControllerForm( @controller = new App.ControllerForm(
model: model:
configure_attributes: configure_attributes configure_attributes: configure_attributes
@ -195,6 +240,7 @@ class Edit extends App.ControllerGenericEdit
onSubmit: (e) => onSubmit: (e) =>
params = @formParam(e.target) params = @formParam(e.target)
params = treeParams(e, params)
# show attributes for create_middle in two column style # show attributes for create_middle in two column style
if params.screens && params.screens.create_middle if params.screens && params.screens.create_middle

View file

@ -6,7 +6,7 @@ class App.TicketCustomer extends App.ControllerModal
content: -> content: ->
configure_attributes = [ configure_attributes = [
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateUser: true }, { name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: true },
] ]
controller = new App.ControllerForm( controller = new App.ControllerForm(
model: model:

View file

@ -221,7 +221,7 @@ class App.TicketOverview extends App.Controller
if @batchCountIndex == @batchCount if @batchCountIndex == @batchCount
App.Event.trigger('overview:fetch') App.Event.trigger('overview:fetch')
) )
return return
if action is 'group_assign' if action is 'group_assign'
@batchCount = items.length @batchCount = items.length

View file

@ -401,10 +401,11 @@ class App.TicketZoom extends App.Controller
nav: @nav nav: @nav
isCustomer: @permissionCheck('ticket.customer') isCustomer: @permissionCheck('ticket.customer')
scrollbarWidth: App.Utils.getScrollBarWidth() scrollbarWidth: App.Utils.getScrollBarWidth()
dir: App.i18n.dir()
) )
new App.TicketZoomOverviewNavigator( new App.TicketZoomOverviewNavigator(
el: elLocal.find('.overview-navigator') el: elLocal.find('.js-overviewNavigatorContainer')
ticket_id: @ticket_id ticket_id: @ticket_id
overview_id: @overview_id overview_id: @overview_id
) )
@ -412,13 +413,13 @@ class App.TicketZoom extends App.Controller
new App.TicketZoomTitle( new App.TicketZoomTitle(
object_id: @ticket_id object_id: @ticket_id
overview_id: @overview_id overview_id: @overview_id
el: elLocal.find('.ticket-title') el: elLocal.find('.js-ticketTitleContainer')
task_key: @task_key task_key: @task_key
) )
new App.TicketZoomMeta( new App.TicketZoomMeta(
object_id: @ticket_id object_id: @ticket_id
el: elLocal.find('.ticket-meta') el: elLocal.find('.js-ticketMetaContainer')
) )
@attributeBar = new App.TicketZoomAttributeBar( @attributeBar = new App.TicketZoomAttributeBar(
@ -445,7 +446,12 @@ class App.TicketZoom extends App.Controller
) )
@highligher = new App.TicketZoomHighlighter( @highligher = new App.TicketZoomHighlighter(
el: elLocal.find('.highlighter') el: elLocal.find('.js-highlighterContainer')
ticket_id: @ticket_id
)
new App.TicketZoomSetting(
el: elLocal.find('.js-settingContainer')
ticket_id: @ticket_id ticket_id: @ticket_id
) )
@ -467,6 +473,7 @@ class App.TicketZoom extends App.Controller
sidebarState: @sidebarState sidebarState: @sidebarState
object_id: @ticket_id object_id: @ticket_id
model: 'Ticket' model: 'Ticket'
query: @query
taskGet: @taskGet taskGet: @taskGet
task_key: @task_key task_key: @task_key
formMeta: @formMeta formMeta: @formMeta
@ -557,14 +564,16 @@ class App.TicketZoom extends App.Controller
return if !@ticket return if !@ticket
currentStoreTicket = @ticket.attributes() currentStoreTicket = @ticket.attributes()
delete currentStoreTicket.article delete currentStoreTicket.article
internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
currentStore = currentStore =
ticket: currentStoreTicket ticket: currentStoreTicket
article: article:
to: '' to: ''
cc: '' cc: ''
subject: ''
type: 'note' type: 'note'
body: '' body: ''
internal: 'true' internal: internal
in_reply_to: '' in_reply_to: ''
if @permissionCheck('ticket.customer') if @permissionCheck('ticket.customer')
@ -575,7 +584,7 @@ class App.TicketZoom extends App.Controller
formCurrent: => formCurrent: =>
currentParams = currentParams =
ticket: @formParam(@el.find('.edit')) ticket: @formParam(@el.find('.edit'))
article: @formParam(@el.find('.article-add')) article: @articleNew.params()
# add attachments if exist # add attachments if exist
attachmentCount = @$('.article-add .textBubble .attachments .attachment').length attachmentCount = @$('.article-add .textBubble .attachments .attachment').length
@ -684,7 +693,7 @@ class App.TicketZoom extends App.Controller
tagAdd: (tag) => tagAdd: (tag) =>
return if !@sidebar return if !@sidebar
return if !@sidebar.reload return if !@sidebar.reload
@sidebar.reload(tagAdd: tag) @sidebar.reload(tagAdd: tag, source: 'macro')
tagRemove: (tag) => tagRemove: (tag) =>
return if !@sidebar return if !@sidebar
return if !@sidebar.reload return if !@sidebar.reload
@ -789,6 +798,9 @@ class App.TicketZoom extends App.Controller
# reset form after save # reset form after save
@reset() @reset()
if @sidebar
@sidebar.commit()
if taskAction is 'closeNextInOverview' if taskAction is 'closeNextInOverview'
if @overview_id if @overview_id
current_position = 0 current_position = 0

View file

@ -391,25 +391,41 @@ class App.TicketZoomArticleActions extends App.Controller
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || '' body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
# check if quote need to be added # check if quote need to be added
selectedText = App.ClipBoard.getSelected() signaturePosition = 'bottom'
if selectedText selected = App.ClipBoard.getSelected('html')
if selected
selected = App.Utils.htmlCleanup(selected).html()
if !selected
selected = App.ClipBoard.getSelected('text')
if selected
selected = App.Utils.textCleanup(selected)
selected = App.Utils.text2html(selected)
# clean selection # full quote, if needed
selectedText = App.Utils.textCleanup(selectedText) if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote')
signaturePosition = 'top'
if article.content_type.match('html')
selected = App.Utils.textCleanup(article.body)
if article.content_type.match('plain')
selected = App.Utils.textCleanup(article.body)
selected = App.Utils.text2html(selected)
# convert to html if selected
selectedText = App.Utils.text2html(selectedText) selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>"
if selectedText
selectedText = "<div><br><br/></div><div><blockquote type=\"cite\">#{selectedText}</blockquote></div><div><br></div>"
# add selected text to body # add selected text to body
body = selectedText + body body = selected + body
articleNew.body = body articleNew.body = body
type = App.TicketArticleType.findByAttribute(name:'email') type = App.TicketArticleType.findByAttribute(name:'email')
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } ) App.Event.trigger('ui::ticket::setArticleType', {
ticket: @ticket
type: type
article: articleNew
signaturePosition: signaturePosition
})
telegramPersonalMessageReply: (e) => telegramPersonalMessageReply: (e) =>
e.preventDefault() e.preventDefault()

View file

@ -28,7 +28,62 @@ class App.TicketZoomArticleNew extends App.Controller
constructor: -> constructor: ->
super super
# set possble article types @internalSelector = true
@type = @defaults['type'] || 'note'
@setPossibleArticleTypes()
if @permissionCheck('ticket.customer')
@internalSelector = false
@textareaHeight =
open: 148
closed: 20
@dragEventCounter = 0
@attachments = []
@render()
if @defaults.body or @isIE10()
@openTextarea(null, true)
# set article type and expand text area
@bind('ui::ticket::setArticleType', (data) =>
return if data.ticket.id.toString() isnt @ticket_id.toString()
@openTextarea(null, true)
for key, value of data.article
if key is 'body'
@$('[data-name="' + key + '"]').html(value)
else
@$('[name="' + key + '"]').val(value).trigger('change')
# preselect article type
@setArticleType(data.type.name, data.signaturePosition)
# set focus at end of field
if data.position is 'end'
@placeCaretAtEnd(@textarea.get(0))
return
# set focus into field
@textarea.focus()
)
# reset new article screen
@bind('ui::ticket::taskReset', (data) =>
return if data.ticket_id.toString() isnt @ticket_id.toString()
@type = 'note'
@defaults = {}
@render()
)
# rerender, e. g. on language change
@bind('ui:rerender', =>
@render()
)
setPossibleArticleTypes: =>
possibleArticleType = possibleArticleType =
note: true note: true
phone: true phone: true
@ -50,12 +105,9 @@ class App.TicketZoomArticleNew extends App.Controller
possibleArticleType['email'] = true possibleArticleType['email'] = true
# gets referenced in @setArticleType # gets referenced in @setArticleType
@internalSelector = true
@type = @defaults['type'] || 'note'
@articleTypes = [] @articleTypes = []
if possibleArticleType.note if possibleArticleType.note
internal = @Config.get('ui_ticket_zoom_article_new_internal') internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
@articleTypes.push { @articleTypes.push {
name: 'note' name: 'note'
icon: 'note' icon: 'note'
@ -64,10 +116,13 @@ class App.TicketZoomArticleNew extends App.Controller
features: ['attachment'] features: ['attachment']
} }
if possibleArticleType.email if possibleArticleType.email
attributes = ['to', 'cc', 'subject']
if !@Config.get('ui_ticket_zoom_article_email_subject')
attributes = ['to', 'cc']
@articleTypes.push { @articleTypes.push {
name: 'email' name: 'email'
icon: 'email' icon: 'email'
attributes: ['to', 'cc'] attributes: attributes
internal: false, internal: false,
features: ['attachment'] features: ['attachment']
} }
@ -80,22 +135,28 @@ class App.TicketZoomArticleNew extends App.Controller
features: [] features: []
} }
if possibleArticleType['twitter status'] if possibleArticleType['twitter status']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push { @articleTypes.push {
name: 'twitter status' name: 'twitter status'
icon: 'twitter' icon: 'twitter'
attributes: [] attributes: []
internal: false, internal: false,
features: ['body:limit'] features: ['body:limit', 'body:initials']
maxTextLength: 140 maxTextLength: 140
warningTextLength: 30 warningTextLength: 30
} }
if possibleArticleType['twitter direct-message'] if possibleArticleType['twitter direct-message']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push { @articleTypes.push {
name: 'twitter direct-message' name: 'twitter direct-message'
icon: 'twitter' icon: 'twitter'
attributes: ['to'] attributes: ['to']
internal: false, internal: false,
features: ['body:limit'] features: ['body:limit', 'body:initials']
maxTextLength: 10000 maxTextLength: 10000
warningTextLength: 500 warningTextLength: 500
} }
@ -130,57 +191,6 @@ class App.TicketZoomArticleNew extends App.Controller
}, },
] ]
if @permissionCheck('ticket.customer')
@internalSelector = false
@textareaHeight =
open: 148
closed: 20
@dragEventCounter = 0
@attachments = []
@render()
if @defaults.body or @isIE10()
@openTextarea(null, true)
# set article type and expand text area
@bind('ui::ticket::setArticleType', (data) =>
return if data.ticket.id.toString() isnt @ticket_id.toString()
@openTextarea(null, true)
for key, value of data.article
if key is 'body'
@$('[data-name="' + key + '"]').html(value)
else
@$('[name="' + key + '"]').val(value).trigger('change')
# preselect article type
@setArticleType(data.type.name)
# set focus at end of field
if data.position is 'end'
@placeCaretAtEnd(@textarea.get(0))
return
# set focus into field
@textarea.focus()
)
# reset new article screen
@bind('ui::ticket::taskReset', (data) =>
return if data.ticket_id.toString() isnt @ticket_id.toString()
@type = 'note'
@defaults = {}
@render()
)
# rerender, e. g. on language change
@bind('ui:rerender', =>
@render()
)
placeCaretAtEnd: (el) -> placeCaretAtEnd: (el) ->
el.focus() el.focus()
if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined' if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined'
@ -229,7 +239,7 @@ class App.TicketZoomArticleNew extends App.Controller
) )
configure_attributes = [ configure_attributes = [
{ name: 'customer_id', display: 'Recipients', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateUser: false }, { name: 'customer_id', display: 'Recipients', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: false },
] ]
controller = new App.ControllerForm( controller = new App.ControllerForm(
@ -300,6 +310,7 @@ class App.TicketZoomArticleNew extends App.Controller
data: data:
ticket: ticket ticket: ticket
user: App.Session.get() user: App.Session.get()
config: App.Config.all()
) )
callback = (ticket) -> callback = (ticket) ->
textModule.reload( textModule.reload(
@ -318,9 +329,6 @@ class App.TicketZoomArticleNew extends App.Controller
params.form_id = @form_id params.form_id = @form_id
params.content_type = 'text/html' params.content_type = 'text/html'
if !params['internal']
params['internal'] = false
if @permissionCheck('ticket.customer') if @permissionCheck('ticket.customer')
sender = App.TicketArticleSender.findByAttribute('name', 'Customer') sender = App.TicketArticleSender.findByAttribute('name', 'Customer')
type = App.TicketArticleType.findByAttribute('name', 'web') type = App.TicketArticleType.findByAttribute('name', 'web')
@ -332,15 +340,20 @@ class App.TicketZoomArticleNew extends App.Controller
params.sender_id = sender.id params.sender_id = sender.id
params.type_id = type.id params.type_id = type.id
if params.internal
params.internal = true
else
params.internal = false
if params.type is 'twitter status' if params.type is 'twitter status'
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false) App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
params.content_type = 'text/plain' params.content_type = 'text/plain'
params.body = "#{App.Utils.html2text(params.body, true)}\n#{@signature.text()}" params.body = App.Utils.html2text(params.body, true)
if params.type is 'twitter direct-message' if params.type is 'twitter direct-message'
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false) App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
params.content_type = 'text/plain' params.content_type = 'text/plain'
params.body = "#{App.Utils.html2text(params.body, true)}\n#{@signature.text()}" params.body = App.Utils.html2text(params.body, true)
if params.type is 'facebook feed comment' if params.type is 'facebook feed comment'
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false) App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
@ -352,6 +365,16 @@ class App.TicketZoomArticleNew extends App.Controller
params.content_type = 'text/plain' params.content_type = 'text/plain'
params.body = App.Utils.html2text(params.body, true) params.body = App.Utils.html2text(params.body, true)
# add initals?
for articleType in @articleTypes
if articleType.name is @type
if _.contains(articleType.features, 'body:initials')
if params.content_type is 'text/html'
params.body = "#{params.body}</br>#{@signature.text()}"
else
params.body = "#{params.body}\n#{@signature.text()}"
break
params params
validate: => validate: =>
@ -411,11 +434,11 @@ class App.TicketZoomArticleNew extends App.Controller
return false return false
if params.type is 'twitter status' if params.type is 'twitter status'
textLength = @maxTextLength - params.body.length textLength = @maxTextLength - App.Utils.textLengthWithUrl(params.body)
return false if textLength < 0 return false if textLength < 0
if params.type is 'twitter direct-message' if params.type is 'twitter direct-message'
textLength = @maxTextLength - params.body.length textLength = @maxTextLength - App.Utils.textLengthWithUrl(params.body)
return false if textLength < 0 return false if textLength < 0
true true
@ -461,13 +484,15 @@ class App.TicketZoomArticleNew extends App.Controller
@$('[name=internal]').val('') @$('[name=internal]').val('')
setArticleType: (type) => setArticleType: (type, signaturePosition = 'bottom') =>
wasScrolledToBottom = @isScrolledToBottom() wasScrolledToBottom = @isScrolledToBottom()
@type = type @type = type
@$('[name=type]').val(type).trigger('change') @$('[name=type]').val(type).trigger('change')
@articleNewEdit.attr('data-type', type) @articleNewEdit.attr('data-type', type)
@$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide') @$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide')
@setPossibleArticleTypes()
# get config # get config
config = {} config = {}
for articleTypeConfig in @articleTypes for articleTypeConfig in @articleTypes
@ -500,7 +525,7 @@ class App.TicketZoomArticleNew extends App.Controller
@$('[data-name=body] [data-signature="true"]').remove() @$('[data-name=body] [data-signature="true"]').remove()
# apply new signature # apply new signature
signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), ticket: ticketCurrent }) signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), ticket: ticketCurrent, config: App.Config.all() })
body = @$('[data-name=body]') body = @$('[data-name=body]')
if App.Utils.signatureCheck(body.html() || '', signatureFinished) if App.Utils.signatureCheck(body.html() || '', signatureFinished)
@ -508,7 +533,10 @@ class App.TicketZoomArticleNew extends App.Controller
body.append('<br><br>') body.append('<br><br>')
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>") signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
App.Utils.htmlStrip(signature) App.Utils.htmlStrip(signature)
body.append(signature) if signaturePosition is 'top'
body.prepend(signature)
else
body.append(signature)
@$('[data-name=body]').replaceWith(body) @$('[data-name=body]').replaceWith(body)
# remove old signature # remove old signature
@ -534,13 +562,28 @@ class App.TicketZoomArticleNew extends App.Controller
for name in articleType.features for name in articleType.features
if name is 'attachment' if name is 'attachment'
@$('.article-attachment, .attachments').removeClass('hide') @$('.article-attachment, .attachments').removeClass('hide')
if name is 'body:initials'
@updateInitials()
if name is 'body:limit' if name is 'body:limit'
@maxTextLength = articleType.maxTextLength @maxTextLength = articleType.maxTextLength
@warningTextLength = articleType.warningTextLength @warningTextLength = articleType.warningTextLength
@delay(@updateLetterCount, 600) @delay(@updateLetterCount, 600)
@updateInitials()
@$('.js-textSizeLimit').removeClass('hide') @$('.js-textSizeLimit').removeClass('hide')
# convert remote src images to data uri
@$('[data-name=body] img').each( (i,image) ->
$image = $(image)
src = $image.attr('src')
if !_.isEmpty(src) && !src.match(/^data:image/i)
canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
ctx = canvas.getContext('2d')
ctx.drawImage(image, 0, 0)
dataURL = canvas.toDataURL()
$image.attr('src', dataURL)
)
@scrollToBottom() if wasScrolledToBottom @scrollToBottom() if wasScrolledToBottom
isScrolledToBottom: -> isScrolledToBottom: ->
@ -557,7 +600,8 @@ class App.TicketZoomArticleNew extends App.Controller
return if !@maxTextLength return if !@maxTextLength
return if !@warningTextLength return if !@warningTextLength
params = @params() params = @params()
textLength = @maxTextLength - params.body.length textLength = App.Utils.textLengthWithUrl(params.body)
textLength = @maxTextLength - textLength
className = switch className = switch
when textLength < 0 then 'label-danger' when textLength < 0 then 'label-danger'
when textLength < @warningTextLength then 'label-warning' when textLength < @warningTextLength then 'label-warning'

View file

@ -0,0 +1,35 @@
class App.TicketZoomSetting extends App.Controller
events:
'click .js-setting': 'show'
constructor: ->
super
return if !@permissionCheck('admin')
@render()
render: ->
@html(App.view('ticket_zoom/setting')())
show: ->
new Modal()
class Modal extends App.ControllerModal
buttonClose: true
buttonCancel: true
buttonSubmit: false
head: 'Settings'
constructor: ->
super
render: =>
super
post: =>
new App.SettingsArea(
area: 'UI::TicketZoom'
el: @el.find('.modal-body')
)
content: ->
App.view('generic/page_loading')()

View file

@ -9,18 +9,32 @@ class App.TicketZoomSidebar extends App.ObserverController
if backend && backend.reload if backend && backend.reload
backend.reload(args) backend.reload(args)
commit: (args) =>
for key, backend of @sidebarBackends
if backend && backend.commit
backend.commit(args)
render: (ticket) => render: (ticket) =>
@sidebarBackends = {} @sidebarBackends ||= {}
@sidebarItems = [] @sidebarItems = []
sidebarBackends = App.Config.get('TicketZoomSidebar') sidebarBackends = App.Config.get('TicketZoomSidebar')
keys = _.keys(sidebarBackends).sort() keys = _.keys(sidebarBackends).sort()
for key in keys for key in keys
@sidebarBackends[key] = new sidebarBackends[key]( if !@sidebarBackends[key] || !@sidebarBackends[key].reload
ticket: ticket @sidebarBackends[key] = new sidebarBackends[key](
taskGet: @taskGet ticket: ticket
formMeta: @formMeta query: @query
markForm: @markForm taskGet: @taskGet
) formMeta: @formMeta
markForm: @markForm
)
else
@sidebarBackends[key].reload(
params: @params
query: @query
formMeta: @formMeta
markForm: @markForm
)
item = @sidebarBackends[key].sidebarItem() item = @sidebarBackends[key].sidebarItem()
if item if item
@sidebarItems.push item @sidebarItems.push item

View file

@ -1,7 +1,7 @@
class SidebarCustomer extends App.Controller class SidebarCustomer extends App.Controller
sidebarItem: => sidebarItem: =>
return if !@permissionCheck('ticket.agent') return if !@permissionCheck('ticket.agent')
{ items = {
head: 'Customer' head: 'Customer'
name: 'customer' name: 'customer'
icon: 'person' icon: 'person'
@ -11,14 +11,16 @@ class SidebarCustomer extends App.Controller
name: 'customer-change' name: 'customer-change'
callback: @changeCustomer callback: @changeCustomer
}, },
{
title: 'Edit Customer'
name: 'customer-edit'
callback: @editCustomer
},
] ]
callback: @showCustomer callback: @showCustomer
} }
return items if @ticket && @ticket.customer_id == 1
items.actions.push {
title: 'Edit Customer'
name: 'customer-edit'
callback: @editCustomer
}
items
showCustomer: (el) => showCustomer: (el) =>
@el = el @el = el

View file

@ -0,0 +1,132 @@
class SidebarIdoit extends App.Controller
sidebarItem: =>
return if !@Config.get('idoit_integration')
{
head: 'i-doit'
name: 'idoit'
icon: 'printer'
actions: [
{
title: 'Change Objects'
name: 'objects-change'
callback: @changeObjects
},
]
callback: @showObjects
}
changeObjects: =>
new App.IdoitObjectSelector(
task_key: @task_key
container: @el.closest('.content')
callback: (objectIds, objectSelectorUi) =>
if @ticket && @ticket.id
@updateTicket(@ticket.id, objectIds, =>
objectSelectorUi.close()
@showObjectsContent(objectIds)
)
return
objectSelectorUi.close()
@showObjectsContent(objectIds)
)
showObjects: (el) =>
@el = el
# show placeholder
@objectIds ||= []
if @ticket && @ticket.preferences && @ticket.preferences.idoit && @ticket.preferences.idoit.object_ids
@objectIds = @ticket.preferences.idoit.object_ids
queryParams = @queryParam()
if queryParams && queryParams.idoit_object_ids
@objectIds.push queryParams.idoit_object_ids
@showObjectsContent()
showObjectsContent: (objectIds) =>
if objectIds
@objectIds = @objectIds.concat(objectIds)
# show placeholder
if _.isEmpty(@objectIds)
@html("<div>#{App.i18n.translateInline('none')}</div>")
return
# ajax call to show items
@ajax(
id: "idoit-#{@task_key}"
type: 'POST'
url: "#{@apiPath}/integration/idoit"
data: JSON.stringify(method: 'cmdb.objects', filter: ids: @objectIds)
success: (data, status, xhr) =>
if data.response
@showList(data.response.result)
return
@showError('Unable to load data...')
error: (xhr, status, error) =>
# do not close window if request is aborted
return if status is 'abort'
# show error message
@showError('Unable to load data...')
)
showList: (objects) =>
list = $(App.view('ticket_zoom/sidebar_idoit')(
objects: objects
))
list.delegate('.js-delete', 'click', (e) =>
e.preventDefault()
objectId = $(e.currentTarget).attr 'data-object-id'
@delete(objectId)
)
@html(list)
showError: (message) =>
@html App.i18n.translateInline(message)
delete: (objectId) =>
localObjects = []
for localObjectId in @objectIds
if objectId.toString() isnt localObjectId.toString()
localObjects.push localObjectId
@objectIds = localObjects
if @ticket && @ticket.id
@updateTicket(@ticket.id, @objectIds)
@showObjectsContent()
commit: (args) =>
return if @ticket && @ticket.id
return if !@objectIds
return if _.isEmpty(@objectIds)
return if !args
return if !args.ticket_id
@updateTicket(args.ticket_id, @objectIds)
updateTicket: (ticket_id, objectIds, callback) =>
App.Ajax.request(
id: "idoit-update-#{ticket_id}"
type: 'POST'
url: "#{@apiPath}/integration/idoit_ticket_update"
data: JSON.stringify(ticket_id: ticket_id, object_ids: objectIds)
success: (data, status, xhr) ->
if callback
callback(objectIds)
error: (xhr, status, details) =>
# do not close window if request is aborted
return if status is 'abort'
# show error message
@log 'errors', details
@notify(
type: 'error'
msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to update object!')
timeout: 6000
)
)
App.Config.set('500-Idoit', SidebarIdoit, 'TicketCreateSidebar')
App.Config.set('500-Idoit', SidebarIdoit, 'TicketZoomSidebar')

View file

@ -1,5 +1,6 @@
class SidebarOrganization extends App.Controller class SidebarOrganization extends App.Controller
sidebarItem: => sidebarItem: =>
return if !@permissionCheck('ticket.agent')
return if !@ticket.organization_id return if !@ticket.organization_id
{ {
head: 'Organization' head: 'Organization'

View file

@ -69,7 +69,7 @@ class SidebarTicket extends App.Controller
if args.tags if args.tags
@tagWidget.reload(args.tags) @tagWidget.reload(args.tags)
if args.tagAdd if args.tagAdd
@tagWidget.add(args.tagAdd) @tagWidget.add(args.tagAdd, args.source)
if args.tagRemove if args.tagRemove
@tagWidget.remove(args.tagRemove) @tagWidget.remove(args.tagRemove)

View file

@ -7,6 +7,12 @@ class App.WidgetAvatar extends App.ObserverController
email: true email: true
image: true image: true
vip: true vip: true
out_of_office: true,
out_of_office_start_at: true,
out_of_office_end_at: true,
out_of_office_replacement_id: true,
active: true
globalRerender: false globalRerender: false
render: (user) => render: (user) =>

View file

@ -5,10 +5,10 @@ class Widget
banner = """ banner = """
| |
| Welcome Zammad Developer! | Welcome Zammad Developer!
| You can enable debugging by the following examples (value is a regex): | You can enable debugging with the following examples (value is a regex):
| |
| App.Log.config('module', '(websocket|delay|interval)') // enable debugging for websocket, delay and interval class | App.Log.config('module', '(websocket|delay|interval)') // enable debugging for websocket, delay and interval class
| App.Log.config('content', 'send') // enable debugging for messages which contains the string 'send' | App.Log.config('content', 'send') // enable debugging for messages which contain the string 'send'
| App.Log.config('banner', false) // disable this banner | App.Log.config('banner', false) // disable this banner
| |
| App.Log.config() // current settings | App.Log.config() // current settings

View file

@ -10,9 +10,9 @@ class Widget
| |
| Hi there, nice to meet you! | Hi there, nice to meet you!
| |
| Visit %chttps://zammad.org/participate%c and let's make Zammad better. | Visit %chttp://zammad.com/jobs%c to learn about our current job openings.
| |
| The Zammad Team. | Your Zammad Team
| |
""" """
console.log(banner, 'text-decoration: underline;', 'text-decoration: none;') console.log(banner, 'text-decoration: underline;', 'text-decoration: none;')

View file

@ -25,6 +25,7 @@ class App.HttpLog extends App.Controller
render: => render: =>
@html App.view('widget/http_log')( @html App.view('widget/http_log')(
records: @records records: @records
description: @description
) )
show: (e) => show: (e) =>

View file

@ -0,0 +1,24 @@
class App.ScriptSnipped extends App.Controller
#events:
# 'click .js-record': 'show'
elements:
'.js-code': 'code'
constructor: ->
super
#@fetch()
@records = []
@render()
render: =>
@html App.view('widget/script_snipped')(
records: @records
description: @description
style: @style
content: @content
)
@code.each (i, block) ->
hljs.highlightBlock block

View file

@ -86,16 +86,16 @@ class App.WidgetTag extends App.Controller
return return
@add(item) @add(item)
add: (items) => add: (items, source = '') =>
for item in items.split(',') for item in items.split(',')
item = item.trim() item = item.trim()
@addItem(item) @addItem(item, source)
addItem: (item) => addItem: (item, source = '') =>
if _.contains(@localTags, item) if _.contains(@localTags, item)
@render() @render()
return return
return if App.Config.get('tag_new') is false && !@possibleTags[item] return if source != 'macro' && App.Config.get('tag_new') is false && !@possibleTags[item]
@localTags.push item @localTags.push item
@render() @render()
App[@object_type].tagAdd(@object.id, item) App[@object_type].tagAdd(@object.id, item)

View file

@ -8,7 +8,7 @@ class App.WidgetTextModule extends App.Controller
# remember instances # remember instances
@bindElements = [] @bindElements = []
if @selector if @selector
@bindElements = @$( @selector ).textmodule() @bindElements = @$(@selector).textmodule()
else else
if @el.attr('contenteditable') if @el.attr('contenteditable')
@bindElements = @el.textmodule() @bindElements = @el.textmodule()

View file

@ -0,0 +1,57 @@
class App.QueueManager
_instance = undefined
@init: ->
_instance ?= new _queueSingleton
@add: (key, data) ->
if _instance == undefined
_instance ?= new _queueSingleton
_instance.add(key, data)
@pull: (key) ->
if _instance == undefined
_instance ?= new _queueSingleton
_instance.pull(key)
@all: (key) ->
if _instance == undefined
_instance ?= new _queueSingleton
_instance.all(key)
@run: (key, callback) ->
if _instance == undefined
_instance ?= new _queueSingleton
_instance.run(key, callback)
class _queueSingleton
constructor: ->
@queues = {}
@queueRunning = {}
add: (key, data) ->
if !@queues[key]
@queues[key] = []
@queues[key].push data
true
pull: (key) ->
return if !@queues[key]
@queues[key].shift()
all: (key) ->
@queues[key]
run: (key, callback) ->
return if !@queues[key]
return if @queueRunning[key]
localQueue = @queues[key]
return if _.isEmpty(localQueue)
@queueRunning[key] = true
loop
callback = localQueue.shift()
callback()
if !localQueue[0]
@queueRunning[key] = false
break
true

View file

@ -5,9 +5,9 @@ class App._CollectionSingletonBase
constructor: -> constructor: ->
@callbacks = {} @callbacks = {}
@counter = 0 @counter = 0
@key = "collection-#{@event}"
# read from cache # read from cache
cache = App.SessionStorage.get("collection-#{@event}") cache = App.SessionStorage.get(@key)
if cache if cache
@set(cache) @set(cache)
@ -73,6 +73,9 @@ class App._CollectionSingletonBase
callback: (data) => callback: (data) =>
for counter, attr of @callbacks for counter, attr of @callbacks
attr.callback(data) callback = ->
if attr.one attr.callback(data)
delete @callbacks[counter] if attr.one
delete @callbacks[counter]
App.QueueManager.add(@key, callback)
App.QueueManager.run(@key)

View file

@ -113,6 +113,10 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
@createToken name, objectId @createToken name, objectId
else else
if object.email if object.email
# quote name for special character
if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/)
name = "\"#{name}\""
name += " <#{object.email}>" name += " <#{object.email}>"
@objectSelect.val(name) @objectSelect.val(name)
@ -390,14 +394,14 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
properties: properties:
translateX: 0 translateX: 0
options: options:
speed: 300 duration: 240
# fade out list # fade out list
@recipientList.velocity @recipientList.velocity
properties: properties:
translateX: '-100%' translateX: '-100%'
options: options:
speed: 300 duration: 240
complete: => @recipientList.height(@organizationList.height()) complete: => @recipientList.height(@organizationList.height())
hideOrganizationMembers: (e) => hideOrganizationMembers: (e) =>
@ -413,7 +417,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
properties: properties:
translateX: 0 translateX: 0
options: options:
speed: 300 duration: 240
# reset list height # reset list height
@recipientList.height('') @recipientList.height('')
@ -423,7 +427,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
properties: properties:
translateX: '100%' translateX: '100%'
options: options:
speed: 300 duration: 240
complete: => @organizationList.addClass('hide') complete: => @organizationList.addClass('hide')
newObject: (e) -> newObject: (e) ->

View file

@ -6,25 +6,25 @@ class App.ClipBoard
_instance ?= new _Singleton _instance ?= new _Singleton
_instance.bind(el) _instance.bind(el)
@getSelected: -> @getSelected: (type) ->
if _instance == undefined if _instance == undefined
_instance ?= new _Singleton _instance ?= new _Singleton
_instance.getSelected() _instance.getSelected(type)
@getSelectedLast: -> @getSelectedLast: (type) ->
if _instance == undefined if _instance == undefined
_instance ?= new _Singleton _instance ?= new _Singleton
_instance.getSelectedLast() _instance.getSelectedLast(type)
@getPosition: (el) -> @getPosition: (el) ->
if _instance == undefined if _instance == undefined
_instance ?= new _Singleton _instance ?= new _Singleton
_instance.getPosition(el) _instance.getPosition(el)
@setPosition: ( el, pos ) -> @setPosition: (el, pos) ->
if _instance == undefined if _instance == undefined
_instance ?= new _Singleton _instance ?= new _Singleton
_instance.setPosition( el, pos ) _instance.setPosition(el, pos)
@keycode: (code) -> @keycode: (code) ->
if _instance == undefined if _instance == undefined
@ -33,54 +33,68 @@ class App.ClipBoard
class _Singleton class _Singleton
constructor: -> constructor: ->
@selection = '' @selection =
@selectionLast = '' html: ''
text: ''
@selectionLast =
html: ''
text: ''
# bind to fill selected text into # bind to fill selected text into
bind: (el) -> bind: (el) ->
$(el).bind('mouseup', =>
# check selection on mouse up # check selection on mouse up
@selection = @_getSelected() $(el).bind('mouseup', =>
if @selection @_updateSelection()
@selectionLast = @selection
) )
$(el).bind('keyup', (e) => $(el).bind('keyup', (e) =>
# check selection on sonder key # check selection on sonder key
if e.keyCode == 91 if e.keyCode == 91
@selection = @_getSelected() @_updateSelection()
if @selection
@selectionLast = @selection
# check selection of arrow keys # check selection of arrow keys
if e.keyCode == 37 || e.keyCode == 38 || e.keyCode == 39 || e.keyCode == 40 if e.keyCode == 37 || e.keyCode == 38 || e.keyCode == 39 || e.keyCode == 40
@selection = @_getSelected() @_updateSelection()
if @selection
@selectionLast = @selection
) )
_updateSelection: =>
for key in ['html', 'text']
@selection[key] = @_getSelected(key)
if @selection[key]
@selectionLast[key] = @selection[key]
# get cross browser selected string # get cross browser selected string
_getSelected: -> _getSelected: (type) ->
text = '' text = ''
html = ''
if window.getSelection if window.getSelection
text = window.getSelection() sel = window.getSelection()
text = sel.toString()
else if document.getSelection else if document.getSelection
text = document.getSelection() sel = document.getSelection()
text = sel.toString()
else if document.selection else if document.selection
text = document.selection.createRange().text sel = document.selection.createRange()
if text text = sel.text
# text = text.toString().trim() if type is 'text'
text = $.trim( text.toString() ) return $.trim(text.toString()) if text
text return ''
if sel && sel.rangeCount
container = document.createElement('div')
for i in [1..sel.rangeCount]
container.appendChild(sel.getRangeAt(i-1).cloneContents())
html = container.innerHTML
html
# get current selection # get current selection
getSelected: -> getSelected: (type) ->
@selection @selection[type]
# get latest selection # get latest selection
getSelectedLast: -> getSelectedLast: (type) ->
@selectionLast @selectionLast[type]
getPosition: (el) -> getPosition: (el) ->
pos = 0 pos = 0
@ -104,13 +118,13 @@ class _Singleton
# IE Support # IE Support
if el.setSelectionRange if el.setSelectionRange
el.focus() el.focus()
el.setSelectionRange( pos, pos ) el.setSelectionRange(pos, pos)
# Firefox support # Firefox support
else if el.createTextRange else if el.createTextRange
range = el.createTextRange() range = el.createTextRange()
range.collapse(true) range.collapse(true)
range.moveEnd( 'character', pos ) range.moveEnd('character', pos)
range.moveStart('character', pos) range.moveStart('character', pos)
range.select() range.select()

View file

@ -33,6 +33,11 @@ class App.ColumnSelect extends Spine.Controller
@select @pickedValue @select @pickedValue
, 300, {trailing: false} , 300, {trailing: false}
if @attribute.onChange
@shadow.on('change', =>
@attribute.onChange(@shadow.val())
)
render: -> render: ->
@values = [] @values = []
_.each @options.attribute.options, (option) => _.each @options.attribute.options, (option) =>

View file

@ -1,10 +1,10 @@
class App.Delay class App.Delay
_instance = undefined _instance = undefined
@set: (callback, timeout, key, level) -> @set: (callback, timeout, key, level, queue) ->
if _instance == undefined if _instance == undefined
_instance ?= new _delaySingleton _instance ?= new _delaySingleton
_instance.set(callback, timeout, key, level) _instance.set(callback, timeout, key, level, queue)
@clear: (key, level) -> @clear: (key, level) ->
if _instance == undefined if _instance == undefined
@ -21,6 +21,11 @@ class App.Delay
_instance ?= new _delaySingleton _instance ?= new _delaySingleton
_instance.reset() _instance.reset()
@count: ->
if _instance == undefined
_instance ?= new _intervalSingleton
_instance.count()
@_all: -> @_all: ->
if _instance == undefined if _instance == undefined
_instance ?= new _delaySingleton _instance ?= new _delaySingleton
@ -32,7 +37,7 @@ class _delaySingleton extends Spine.Module
constructor: -> constructor: ->
@levelStack = {} @levelStack = {}
set: (callback, timeout, key, level) => set: (callback, timeout, key, level, queue) =>
if !level if !level
level = '_all' level = '_all'
@ -44,11 +49,15 @@ class _delaySingleton extends Spine.Module
key = Math.floor(Math.random() * 99999) key = Math.floor(Math.random() * 99999)
# setTimeout # setTimeout
@log 'debug', 'set', key, timeout, level, callback @log 'debug', 'set', key, timeout, level, callback, queue
call = => localCallback = =>
@clear(key, level) @clear(key, level)
callback() if queue
delay_id = setTimeout(call, timeout) App.QueueManager.add('delay', callback)
App.QueueManager.run('delay')
else
callback()
delay_id = setTimeout(localCallback, timeout)
# remember all delays # remember all delays
if !@levelStack[level] if !@levelStack[level]
@ -93,6 +102,13 @@ class _delaySingleton extends Spine.Module
@levelStack[level] = {} @levelStack[level] = {}
true true
count: =>
return 0 if !@levelStack
count = 0
for levelName, levelValue of @levelStack
count += Object.keys(levelValue).length
count
_all: => _all: =>
@levelStack @levelStack

View file

@ -30,6 +30,11 @@ class App.i18n
_instance ?= new _i18nSingleton() _instance ?= new _i18nSingleton()
_instance.date(args, offset) _instance.date(args, offset)
@dir: ->
if _instance == undefined
_instance ?= new _i18nSingleton()
_instance.dir()
@get: -> @get: ->
if _instance == undefined if _instance == undefined
_instance ?= new _i18nSingleton() _instance ?= new _i18nSingleton()
@ -88,6 +93,10 @@ class _i18nSingleton extends Spine.Module
@_notTranslated = {} @_notTranslated = {}
@dateFormat = 'yyyy-mm-dd' @dateFormat = 'yyyy-mm-dd'
@timestampFormat = 'yyyy-mm-dd HH:MM' @timestampFormat = 'yyyy-mm-dd HH:MM'
@dirToSet = 'ltr'
dir: ->
@dirToSet
get: -> get: ->
@locale @locale
@ -96,12 +105,15 @@ class _i18nSingleton extends Spine.Module
# prepare locale # prepare locale
localeToSet = localeToSet.toLowerCase() localeToSet = localeToSet.toLowerCase()
@dirToSet = 'ltr'
# check if locale exists # check if locale exists
localeFound = false localeFound = false
locales = App.Locale.all() locales = App.Locale.all()
for locale in locales for locale in locales
if locale.locale is localeToSet if locale.locale is localeToSet
localeToSet = locale.locale
@dirToSet = locale.dir
localeFound = true localeFound = true
# try aliases # try aliases
@ -109,6 +121,8 @@ class _i18nSingleton extends Spine.Module
for locale in locales for locale in locales
if locale.alias is localeToSet if locale.alias is localeToSet
localeToSet = locale.locale localeToSet = locale.locale
@dirToSet = locale.dir
localeFound = true
# if no locale and no alias was found, try to find correct one # if no locale and no alias was found, try to find correct one
if !localeFound if !localeFound
@ -118,15 +132,9 @@ class _i18nSingleton extends Spine.Module
for locale in locales for locale in locales
if locale.alias is localeToSet if locale.alias is localeToSet
localeToSet = locale.locale localeToSet = locale.locale
@dirToSet = locale.dir
localeFound = true localeFound = true
# try to find by locale
if !localeFound
for locale in locales
if locale.locale is localeToSet
localeToSet = locale.locale
localeFound = true
# check if locale need to be changed # check if locale need to be changed
return if localeToSet is @locale return if localeToSet is @locale
@ -136,8 +144,9 @@ class _i18nSingleton extends Spine.Module
# set if not translated should be logged # set if not translated should be logged
@_notTranslatedLog = @notTranslatedFeatureEnabled(@locale) @_notTranslatedLog = @notTranslatedFeatureEnabled(@locale)
# set lang attribute of html tag # set lang and dir attribute of html tag
$('html').prop('lang', @locale.substr(0, 2) ) $('html').prop('lang', localeToSet.substr(0, 2))
$('html').prop('dir', @dirToSet)
@mapString = {} @mapString = {}
App.Ajax.request( App.Ajax.request(

View file

@ -1,10 +1,10 @@
class App.Interval class App.Interval
_instance = undefined _instance = undefined
@set: (callback, timeout, key, level) -> @set: (callback, timeout, key, level, queue) ->
if _instance == undefined if _instance == undefined
_instance ?= new _intervalSingleton _instance ?= new _intervalSingleton
_instance.set(callback, timeout, key, level) _instance.set(callback, timeout, key, level, queue)
@clear: (key, level) -> @clear: (key, level) ->
if _instance == undefined if _instance == undefined
@ -21,6 +21,11 @@ class App.Interval
_instance ?= new _intervalSingleton _instance ?= new _intervalSingleton
_instance.reset() _instance.reset()
@count: ->
if _instance == undefined
_instance ?= new _intervalSingleton
_instance.count()
@_all: -> @_all: ->
if _instance == undefined if _instance == undefined
_instance ?= new _intervalSingleton _instance ?= new _intervalSingleton
@ -32,7 +37,7 @@ class _intervalSingleton extends Spine.Module
constructor: -> constructor: ->
@levelStack = {} @levelStack = {}
set: (callback, timeout, key, level) => set: (callback, timeout, key, level, queue) =>
if !level if !level
level = '_all' level = '_all'
@ -44,9 +49,15 @@ class _intervalSingleton extends Spine.Module
key = Math.floor(Math.random() * 99999) key = Math.floor(Math.random() * 99999)
# setTimeout # setTimeout
@log 'debug', 'set', key, timeout, level, callback @log 'debug', 'set', key, timeout, level, callback, queue
callback() localCallback = ->
interval_id = setInterval(callback, timeout) if queue
App.QueueManager.add('interval', callback)
App.QueueManager.run('interval')
else
callback()
localCallback()
interval_id = setInterval(localCallback, timeout)
# remember all interval # remember all interval
if !@levelStack[level] if !@levelStack[level]
@ -91,5 +102,12 @@ class _intervalSingleton extends Spine.Module
@levelStack[level] = {} @levelStack[level] = {}
true true
count: =>
return 0 if !@levelStack
count = 0
for levelName, levelValue of @levelStack
count += Object.keys(levelValue).length
count
_all: => _all: =>
@levelStack @levelStack

View file

@ -71,7 +71,10 @@ class _Singleton
callback: (view, data) => callback: (view, data) =>
for counter, meta of @callbacks for counter, meta of @callbacks
if meta.view is view if meta.view is view
meta.callback(data) callback = ->
meta.callback(data)
App.QueueManager.add('ticket_overviews', callback)
App.QueueManager.run('ticket_overviews')
class App.OverviewListCollection class App.OverviewListCollection
_instance = new _Singleton _instance = new _Singleton

View file

@ -38,11 +38,16 @@ class App.PrettyDate
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
month = months[created.getMonth()] month = months[created.getMonth()]
# for less than 7 days # for less than 6 days
if diff < (60 * 60 * 24 * 7) # weekday HH::MM
if diff < (60 * 60 * 24 * 6)
string = "#{App.i18n.translateInline(weekday)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}" string = "#{App.i18n.translateInline(weekday)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
else if diff < (60 * 60 * 24 * 7) * 365 # if it was this year
# weekday DD. MM HH::MM
else if created.getYear() is current.getYear()
string = "#{App.i18n.translateInline(weekday)} #{created.getDate()}. #{App.i18n.translateInline(month)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}" string = "#{App.i18n.translateInline(weekday)} #{created.getDate()}. #{App.i18n.translateInline(month)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
# if it was the year before
# weekday YYYY-MM-DD HH::MM
else else
string = "#{App.i18n.translateInline(weekday)} #{App.i18n.translateTimestamp(time)}" string = "#{App.i18n.translateInline(weekday)} #{App.i18n.translateTimestamp(time)}"
if escalation if escalation

View file

@ -1,19 +1,26 @@
class App.SearchableSelect extends Spine.Controller class App.SearchableSelect extends Spine.Controller
events: events:
'input .js-input': 'onInput' 'input .js-input': 'onInput'
'blur .js-input': 'onBlur' 'blur .js-input': 'onBlur'
'focus .js-input': 'onFocus' 'focus .js-input': 'onFocus'
'click .js-option': 'selectItem' 'click .js-option': 'selectItem'
'mouseenter .js-option': 'highlightItem' 'click .js-enter': 'navigateIn'
'shown.bs.dropdown': 'onDropdownShown' 'click .js-back': 'navigateOut'
'hidden.bs.dropdown': 'onDropdownHidden' 'mouseenter .js-option': 'highlightItem'
'mouseenter .js-enter': 'highlightItem'
'mouseenter .js-back': 'highlightItem'
'shown.bs.dropdown': 'onDropdownShown'
'hidden.bs.dropdown': 'onDropdownHidden'
'keyup .js-input': 'onKeyUp'
elements: elements:
'.js-option': 'option_items' '.js-dropdown': 'dropdown'
'.js-option, .js-enter': 'optionItems'
'.js-input': 'input' '.js-input': 'input'
'.js-shadow': 'shadowInput' '.js-shadow': 'shadowInput'
'.js-optionsList': 'optionsList' '.js-optionsList': 'optionsList'
'.js-optionsSubmenu': 'optionsSubmenu'
'.js-autocomplete-invisible': 'invisiblePart' '.js-autocomplete-invisible': 'invisiblePart'
'.js-autocomplete-visible': 'visiblePart' '.js-autocomplete-visible': 'visiblePart'
@ -27,32 +34,99 @@ class App.SearchableSelect extends Spine.Controller
@render() @render()
render: -> render: ->
firstSelected = _.find @options.attribute.options, (option) -> option.selected firstSelected = _.find @attribute.options, (option) -> option.selected
if firstSelected if firstSelected
@options.attribute.valueName = firstSelected.name @attribute.valueName = firstSelected.name
@options.attribute.value = firstSelected.value @attribute.value = firstSelected.value
else if @options.attribute.unknown && @options.attribute.value else if @attribute.unknown && @attribute.value
@options.attribute.valueName = @options.attribute.value @attribute.valueName = @attribute.value
else if @hasSubmenu @attribute.options
@attribute.valueName = @getName @attribute.value, @attribute.options
@options.attribute.renderedOptions = App.view('generic/searchable_select_options') @html App.view('generic/searchable_select')
options: @options.attribute.options attribute: @attribute
options: @renderAllOptions '', @attribute.options, 0
submenus: @renderSubmenus @attribute.options
@html App.view('generic/searchable_select')( @options.attribute ) # initial data
@currentMenu = @findMenuContainingValue @attribute.value
@level = @getIndex @currentMenu
@input.on 'keydown', @navigate renderSubmenus: (options) ->
html = ''
if options
for option in options
if option.children
html += App.view('generic/searchable_select_submenu')
options: @renderOptions(option.children)
parentValue: option.value
title: option.name
if @hasSubmenu(option.children)
html += @renderSubmenus option.children
html
hasSubmenu: (options) ->
return false if !options
for option in options
return true if option.children
return false
getName: (value, options) ->
for option in options
if option.value is value
return option.name
if option.children
name = @getName value, option.children
return name if name isnt undefined
undefined
renderOptions: (options) ->
html = ''
for option in options
html += App.view('generic/searchable_select_option')
option: option
class: if option.children then 'js-enter' else 'js-option'
html
renderAllOptions: (parentName, options, level) ->
html = ''
if options
for option in options
className = if option.children then 'js-enter' else 'js-option'
if level && level > 0
className += ' is-hidden is-child'
html += App.view('generic/searchable_select_option')
option: option
class: className
detail: parentName
if option.children
html += @renderAllOptions "#{parentName}#{option.name}", option.children, level+1
html
onDropdownShown: => onDropdownShown: =>
@input.on 'click', @stopPropagation @input.on 'click', @stopPropagation
@highlightFirst() @highlightFirst()
$(document).on 'keydown.searchable_select', @navigate
if @level > 0
@showSubmenu(@currentMenu)
@isOpen = true @isOpen = true
onDropdownHidden: => onDropdownHidden: =>
@input.off 'click', @stopPropagation @input.off 'click', @stopPropagation
@option_items.removeClass '.is-active' @unhighlightCurrentItem()
$(document).off 'keydown.searchable_select'
@isOpen = false @isOpen = false
onKeyUp: =>
return if @input.val().trim() isnt ''
@shadowInput.val('')
toggle: => toggle: =>
@currentItem = null
@$('[data-toggle="dropdown"]').dropdown('toggle') @$('[data-toggle="dropdown"]').dropdown('toggle')
stopPropagation: (event) -> stopPropagation: (event) ->
@ -62,8 +136,8 @@ class App.SearchableSelect extends Spine.Controller
switch event.keyCode switch event.keyCode
when 40 then @nudge event, 1 # down when 40 then @nudge event, 1 # down
when 38 then @nudge event, -1 # up when 38 then @nudge event, -1 # up
when 39 then @fillWithAutocompleteSuggestion event # right when 39 then @autocompleteOrNavigateIn event # right
when 37 then @fillWithAutocompleteSuggestion event # left when 37 then @autocompleteOrNavigateOut event # left
when 13 then @onEnter event when 13 then @onEnter event
when 27 then @onEscape() when 27 then @onEscape()
when 9 then @onTab event when 9 then @onTab event
@ -71,12 +145,20 @@ class App.SearchableSelect extends Spine.Controller
onEscape: -> onEscape: ->
@toggle() if @isOpen @toggle() if @isOpen
getCurrentOptions: ->
@currentMenu.find('.js-option, .js-enter, .js-back')
getOptionIndex: (menu, value) ->
menu.find('.js-option, .js-enter').filter("[data-value=\"#{value}\"]").index()
nudge: (event, direction) -> nudge: (event, direction) ->
return @toggle() if not @isOpen return @toggle() if not @isOpen
options = @getCurrentOptions()
event.preventDefault() event.preventDefault()
visibleOptions = @option_items.not('.is-hidden') visibleOptions = options.not('.is-hidden')
highlightedItem = @option_items.filter('.is-active') highlightedItem = options.filter('.is-active')
currentPosition = visibleOptions.index(highlightedItem) currentPosition = visibleOptions.index(highlightedItem)
currentPosition += direction currentPosition += direction
@ -84,10 +166,24 @@ class App.SearchableSelect extends Spine.Controller
return if currentPosition < 0 return if currentPosition < 0
return if currentPosition > visibleOptions.size() - 1 return if currentPosition > visibleOptions.size() - 1
@option_items.removeClass('is-active') @unhighlightCurrentItem()
visibleOptions.eq(currentPosition).addClass('is-active') @currentItem = visibleOptions.eq(currentPosition)
@currentItem.addClass('is-active')
@clearAutocomplete() @clearAutocomplete()
autocompleteOrNavigateIn: (event) ->
if @currentItem.hasClass('js-enter')
@navigateIn(event)
else
@fillWithAutocompleteSuggestion(event)
autocompleteOrNavigateOut: (event) ->
# if we're in a depth then navigateOut
if @level != 0
@navigateOut(event)
else
@fillWithAutocompleteSuggestion(event)
fillWithAutocompleteSuggestion: (event) -> fillWithAutocompleteSuggestion: (event) ->
if !@suggestion if !@suggestion
return return
@ -124,16 +220,101 @@ class App.SearchableSelect extends Spine.Controller
@invisiblePart.text('') @invisiblePart.text('')
selectItem: (event) -> selectItem: (event) ->
return if !event.currentTarget.textContent
@input.val event.currentTarget.textContent.trim() @input.val event.currentTarget.textContent.trim()
@input.trigger('change') @input.trigger('change')
@shadowInput.val event.currentTarget.getAttribute('data-value') @shadowInput.val event.currentTarget.getAttribute('data-value')
@shadowInput.trigger('change') @shadowInput.trigger('change')
navigateIn: (event) ->
event.stopPropagation()
@selectItem(event)
@navigateDepth(1)
navigateOut: (event) ->
event.stopPropagation()
@navigateDepth(-1)
navigateDepth: (dir) ->
return if @animating
if dir > 0
target = @currentItem.attr('data-value')
target_menu = @optionsSubmenu.filter("[data-parent-value=\"#{target}\"]")
else
target_menu = @findMenuContainingValue @currentMenu.attr('data-parent-value')
@animateToSubmenu(target_menu, dir)
@level+=dir
animateToSubmenu: (target_menu, direction) ->
@animating = true
target_menu.prop('hidden', false)
@dropdown.height(Math.max(target_menu.height(), @currentMenu.height()))
oldCurrentItem = @currentItem
@currentMenu.data('current_item_index', @currentItem.index())
# default: 1 (first item after the back button)
target_item_index = target_menu.data('current_item_index') || 1
# if the direction is out then we know the target item -> its the parent item
if direction is -1
value = @currentMenu.attr('data-parent-value')
target_item_index = @getOptionIndex(target_menu, value)
@currentItem = target_menu.children().eq(target_item_index)
@currentItem.addClass('is-active')
target_menu.velocity
properties:
translateX: [0, direction*100+'%']
options:
duration: 240
@currentMenu.velocity
properties:
translateX: [direction*-100+'%', 0]
options:
duration: 240
complete: =>
oldCurrentItem.removeClass('is-active')
$.Velocity.hook(@currentMenu, 'translateX', '')
@currentMenu.prop('hidden', true)
@dropdown.height(target_menu.height())
@currentMenu = target_menu
@animating = false
showSubmenu: (menu) ->
@currentMenu.prop('hidden', true)
menu.prop('hidden', false)
@dropdown.height(menu.height())
findMenuContainingValue: (value) ->
return @optionsList if !value
# in case of numbers
if !value.split && value.toString
value = value.toString()
path = value.split('::')
if path.length == 1
return @optionsList
else
path.pop()
return @optionsSubmenu.filter("[data-parent-value=\"#{path.join('::')}\"]")
getIndex: (menu) ->
parentValue = menu.attr('data-parent-value')
return 0 if !parentValue
return parentValue.split('::').length
onTab: (event) -> onTab: (event) ->
return if not @isOpen return if not @isOpen
event.preventDefault() event.preventDefault()
onEnter: (event) -> onEnter: (event) ->
if @currentItem
if @currentItem.hasClass('js-back')
return @navigateOut(event)
@clearAutocomplete() @clearAutocomplete()
if not @isOpen if not @isOpen
@ -144,15 +325,22 @@ class App.SearchableSelect extends Spine.Controller
event.preventDefault() event.preventDefault()
selected = @option_items.filter('.is-active') if @currentItem || !@attribute.unknown
if selected.length || !@options.attribute.unknown valueName = @currentItem.text().trim()
valueName = selected.text().trim() value = @currentItem.attr('data-value')
value = selected.attr('data-value')
@input.val valueName @input.val valueName
@shadowInput.val value @shadowInput.val value
@input.trigger('change') @input.trigger('change')
@shadowInput.trigger('change') @shadowInput.trigger('change')
if @currentItem
if @currentItem.hasClass('js-enter')
@navigateIn(event)
@currentItem = null
return
@currentItem = null
@toggle() @toggle()
onBlur: -> onBlur: ->
@ -169,32 +357,46 @@ class App.SearchableSelect extends Spine.Controller
@query = @input.val() @query = @input.val()
@filterByQuery @query @filterByQuery @query
if @options.attribute.unknown if @attribute.unknown
@shadowInput.val @query @shadowInput.val @query
filterByQuery: (query) -> filterByQuery: (query) ->
query = escapeRegExp(query) query = escapeRegExp(query)
regex = new RegExp(query.split(' ').join('.*'), 'i') regex = new RegExp(query.split(' ').join('.*'), 'i')
@option_items @optionsList.addClass 'is-filtered'
@optionItems
.addClass 'is-hidden' .addClass 'is-hidden'
.filter -> .filter ->
@textContent.match(regex) @textContent.match(regex)
.removeClass 'is-hidden' .removeClass 'is-hidden'
if @options.attribute.unknown && @option_items.length == @option_items.filter('.is-hidden').length if !query
@option_items.removeClass 'is-hidden' @optionItems.filter('.is-child').addClass 'is-hidden'
@option_items.removeClass 'is-active'
# if all are hidden
if @attribute.unknown && @optionItems.length == @optionItems.filter('.is-hidden').length
@optionItems.not('.is-child').removeClass 'is-hidden'
@unhighlightCurrentItem()
@optionsList.removeClass 'is-filtered'
else else
@highlightFirst(true) @highlightFirst(true)
highlightFirst: (autocomplete) -> highlightFirst: (autocomplete) ->
first = @option_items.removeClass('is-active').not('.is-hidden').first() @unhighlightCurrentItem()
first.addClass 'is-active' @currentItem = @getCurrentOptions().not('.is-hidden').first()
@currentItem.addClass 'is-active'
if autocomplete if autocomplete
@autocomplete first.attr('data-value'), first.text().trim() @autocomplete @currentItem.attr('data-value'), @currentItem.text().trim()
highlightItem: (event) => highlightItem: (event) =>
@option_items.removeClass('is-active') @unhighlightCurrentItem()
$(event.currentTarget).addClass('is-active') @currentItem = $(event.currentTarget)
@currentItem.addClass('is-active')
unhighlightCurrentItem: ->
return if !@currentItem
@currentItem.removeClass('is-active')
@currentItem = null

View file

@ -1,5 +1,80 @@
# coffeelint: disable=no_unnecessary_double_quotes # coffeelint: disable=no_unnecessary_double_quotes
class App.Utils class App.Utils
@mapTagAttributes:
'TABLE': ['align', 'bgcolor', 'border', 'cellpadding', 'cellspacing', 'frame', 'rules', 'sortable', 'summary', 'width', 'style']
'TD': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'valign', 'width', 'style']
'TH': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'scope', 'sorted', 'valign', 'width', 'style']
'TR': ['width', 'style']
@mapCss:
'TABLE': [
'background', 'background-color', 'color', 'font-size', 'vertical-align',
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]
'TH': [
'background', 'background-color', 'color', 'font-size', 'vertical-align',
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]
'TR': [
'background', 'background-color', 'color', 'font-size', 'vertical-align',
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]
'TD': [
'background', 'background-color', 'color', 'font-size', 'vertical-align',
'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left',
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]
# textCleand = App.Utils.textCleanup(rawText) # textCleand = App.Utils.textCleanup(rawText)
@textCleanup: (ascii) -> @textCleanup: (ascii) ->
@ -49,10 +124,12 @@ class App.Utils
@linkify: (string) -> @linkify: (string) ->
window.linkify(string) window.linkify(string)
# htmlEscapedAndLinkified = App.Utils.linkify(rawText) # htmlEscapedAndPhoneified = App.Utils.phoneify(rawText)
@phoneify: (string) -> @phoneify: (string) ->
string = string.replace(/\s+/g, '') return string if _.isEmpty(string)
"tel://#{encodeURIComponent(string)}" string = string.replace(/[^0-9,\+,#,\*]+/g, '')
.replace(/(.)\+/, '$1')
"tel:#{string}"
# wrappedText = App.Utils.wrap(rawText, maxLineLength) # wrappedText = App.Utils.wrap(rawText, maxLineLength)
@wrap: (ascii, max = 82) -> @wrap: (ascii, max = 82) ->
@ -125,6 +202,7 @@ class App.Utils
child = el.firstChild child = el.firstChild
break if !child break if !child
break if child.nodeType isnt 1 || child.tagName isnt 'BR' break if child.nodeType isnt 1 || child.tagName isnt 'BR'
break if !child.remove
child.remove() child.remove()
loop loop
@ -133,6 +211,7 @@ class App.Utils
child = el.lastChild child = el.lastChild
break if !child break if !child
break if child.nodeType isnt 1 || child.tagName isnt 'BR' break if child.nodeType isnt 1 || child.tagName isnt 'BR'
break if !child.remove
child.remove() child.remove()
# true|false = App.Utils.htmlLastLineEmpty(element) # true|false = App.Utils.htmlLastLineEmpty(element)
@ -155,12 +234,12 @@ class App.Utils
@_removeWordMarkup(html) @_removeWordMarkup(html)
# remove tags, keep content # remove tags, keep content
html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6').replaceWith( -> html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, th, td, h1, h2, h3, h4, h5, h6').replaceWith( ->
$(@).contents() $(@).contents()
) )
# remove tags & content # remove tags & content
html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6, br, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head').remove() html.find('div, span, p, li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, table, thead, tbody, tr, th, td, h1, h2, h3, h4, h5, h6, br, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head').remove()
html html
@ -172,20 +251,19 @@ class App.Utils
# remove comments # remove comments
@_removeComments(html) @_removeComments(html)
# remove style and class
if parent
@_removeAttributes(html)
# remove work markup # remove work markup
@_removeWordMarkup(html) @_removeWordMarkup(html)
# remove tags, keep content # remove tags, keep content
html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6').replaceWith( -> html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, th, td, h1, h2, h3, h4, h5, h6').replaceWith( ->
$(@).contents() $(@).contents()
) )
# remove tags & content # remove tags & content
html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, td, h1, h2, h3, h4, h5, h6, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head').remove() html.find('li, ul, ol, a, b, u, i, label, small, strong, strike, pre, code, center, blockquote, form, fieldset, textarea, font, address, table, thead, tbody, tr, th, td, h1, h2, h3, h4, h5, h6, hr, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head').remove()
# remove style and class
@_removeAttributes(html, parent)
html html
@ -197,9 +275,6 @@ class App.Utils
# remove comments # remove comments
@_removeComments(html) @_removeComments(html)
# remove style and class
@_removeAttributes(html)
# remove work markup # remove work markup
@_removeWordMarkup(html) @_removeWordMarkup(html)
@ -230,6 +305,9 @@ class App.Utils
# remove tags & content # remove tags & content
html.find('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove() html.find('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove()
# remove style and class
@_cleanAttributes(html)
html html
@_checkTypeOf: (item) -> @_checkTypeOf: (item) ->
@ -250,26 +328,64 @@ class App.Utils
catch err catch err
return $("<div>#{item}</div>") return $("<div>#{item}</div>")
@_removeAttributes: (html, parent = true) -> @_cleanAttribute: (element) ->
return if !element
if @mapTagAttributes[element.nodeName]
atts = element.attributes
for att in atts
if att && att.name && !_.contains(@mapTagAttributes[element.nodeName], att.name)
element.removeAttribute(att.name)
else
@_removeAttribute(element)
if @mapCss[element.nodeName]
elementStyle = element.style
styleOld = ''
for prop in elementStyle
styleOld += "#{prop}:#{elementStyle[prop]};"
if styleOld && styleOld.split
styleNew = ''
for local_pear in styleOld.split(';')
prop = local_pear.split(':')
if prop[0] && prop[0].trim
key = prop[0].trim()
if _.contains(@mapCss[element.nodeName], key)
styleNew += "#{local_pear};"
if styleNew isnt ''
element.setAttribute('style', styleNew)
else
element.removeAttribute('style')
@_cleanAttributes: (html, parent = true) ->
if parent if parent
html.find('*') html.each((index, element) => @_cleanAttribute(element) )
.removeAttr('style') html.find('*').each((index, element) => @_cleanAttribute(element) )
.removeAttr('class')
.removeAttr('title')
.removeAttr('lang')
.removeAttr('type')
.removeAttr('id')
.removeAttr('wrap')
.removeAttrs(/data-/)
html html
.removeAttr('style')
@_removeAttribute: (element) ->
return if !element
$element = $(element)
for att in element.attributes
if att && att.name
element.removeAttribute(att.name)
#$element.removeAttr(att.name)
$element.removeAttr('style')
.removeAttr('class') .removeAttr('class')
.removeAttr('title')
.removeAttr('lang') .removeAttr('lang')
.removeAttr('type') .removeAttr('type')
.removeAttr('align')
.removeAttr('id') .removeAttr('id')
.removeAttr('wrap') .removeAttr('wrap')
.removeAttr('title')
.removeAttrs(/data-/) .removeAttrs(/data-/)
@_removeAttributes: (html, parent = true) ->
if parent
html.each((index, element) => @_removeAttribute(element) )
html.find('*').each((index, element) => @_removeAttribute(element) )
html html
@_removeComments: (html) -> @_removeComments: (html) ->
@ -535,6 +651,7 @@ class App.Utils
# textReplaced = App.Utils.replaceTags( template, { user: { firstname: 'Bob', lastname: 'Smith' } } ) # textReplaced = App.Utils.replaceTags( template, { user: { firstname: 'Bob', lastname: 'Smith' } } )
@replaceTags: (template, objects) -> @replaceTags: (template, objects) ->
template = template.replace( /#\{\s{0,2}(.+?)\s{0,2}\}/g, (index, key) -> template = template.replace( /#\{\s{0,2}(.+?)\s{0,2}\}/g, (index, key) ->
key = key.replace(/<.+?>/g, '')
levels = key.split(/\./) levels = key.split(/\./)
dataRef = objects dataRef = objects
for level in levels for level in levels
@ -781,3 +898,10 @@ class App.Utils
result = newOrderMethod(a, b, applyOrder) result = newOrderMethod(a, b, applyOrder)
return false if !result return false if !result
applyOrder applyOrder
@textLengthWithUrl: (text, url_max_length = 23) ->
length = 0
return length if !text
placeholder = Array(url_max_length + 1).join('X')
text = text.replace(/http(s|):\/\/[-A-Za-z0-9+&@#\/%?=~_\|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/img, placeholder)
text.length

View file

@ -70,8 +70,7 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
options.push data options.push data
# fill template with gathered options # fill template with gathered options
@optionsList.html App.view('generic/searchable_select_options') @optionsList.html @renderOptions options
options: options
# refresh elements # refresh elements
@refreshElements() @refreshElements()

File diff suppressed because one or more lines are too long

View file

@ -88,7 +88,7 @@
// handle enter // handle enter
this.$element.on('keydown', function (e) { this.$element.on('keydown', function (e) {
_this.log('keydown', e.keyCode) _this.log('keydown', e.keyCode)
if ( _this.preventInput ) { if (_this.preventInput) {
this.log('preventInput', _this.preventInput) this.log('preventInput', _this.preventInput)
return return
} }
@ -97,18 +97,29 @@
if (e.keyCode === 13) { if (e.keyCode === 13) {
// disbale multi line // disbale multi line
if ( !_this.options.multiline ) { if (!_this.options.multiline) {
e.preventDefault() e.preventDefault()
return return
} }
// break <blockquote> after enter on empty line // break <blockquote> after enter on empty line
sel = window.getSelection() sel = window.getSelection()
node = $(sel.anchorNode) if (sel) {
if (node.parent().is('blockquote')) { node = $(sel.anchorNode)
if (node && node.parent() && node.parent().is('blockquote')) {
e.preventDefault()
document.execCommand('Insertparagraph')
document.execCommand('Outdent')
return
}
}
// behavior to enter new line on alt+enter
// on alt + enter not realy newline is fired, to make
// it compat. to other systems, do it here
if (!e.shiftKey && e.altKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault() e.preventDefault()
document.execCommand('Insertparagraph') _this.paste('<br><br>')
document.execCommand('Outdent')
return return
} }
} }
@ -237,7 +248,7 @@
// limit check // limit check
if ( !_this.allowKey(e) ) { if ( !_this.allowKey(e) ) {
if ( !_this.maxLengthOk( 1 ) ) { if ( !_this.maxLengthOk(1) ) {
e.preventDefault() e.preventDefault()
return return
} }
@ -254,6 +265,9 @@
if (e.clipboardData) { // ie if (e.clipboardData) { // ie
clipboardData = e.clipboardData clipboardData = e.clipboardData
} }
else if (window.clipboardData) { // ie
clipboardData = window.clipboardData
}
else if (e.originalEvent.clipboardData) { // other browsers else if (e.originalEvent.clipboardData) { // other browsers
clipboardData = e.originalEvent.clipboardData clipboardData = e.originalEvent.clipboardData
} }
@ -292,7 +306,7 @@
else { else {
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">" img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
} }
document.execCommand('insertHTML', false, img) _this.paste(img)
} }
// resize if to big // resize if to big
@ -307,15 +321,23 @@
} }
// check existing + paste text for limit // check existing + paste text for limit
var text = clipboardData.getData('text/html') var text, docType
var docType = 'html' try {
if (!text || text.length === 0) { text = clipboardData.getData('text/html')
docType = 'text' docType = 'html'
text = clipboardData.getData('text/plain') if (!text || text.length === 0) {
docType = 'text'
text = clipboardData.getData('text/plain')
}
if (!text || text.length === 0) {
docType = 'text2'
text = clipboardData.getData('text')
}
} }
if (!text || text.length === 0) { catch (e) {
docType = 'text2' console.log('Sorry, can\'t insert markup because browser is not supporting it.')
text = clipboardData.getData('text') docType = 'text3'
text = clipboardData.getData('text')
} }
_this.log('paste', docType, text) _this.log('paste', docType, text)
@ -355,7 +377,8 @@
// cleanup // cleanup
text = App.Utils.removeEmptyLines(text) text = App.Utils.removeEmptyLines(text)
_this.log('insert', text) _this.log('insert', text)
document.execCommand('insertHTML', false, text)
_this.paste(text)
return true return true
}) })
@ -525,7 +548,30 @@
} }
} }
$.fn[pluginName] = function ( options ) { // paste some content
Plugin.prototype.paste = function(string) {
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
// IE <= 10
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange()
if (range.pasteHTML) {
range.pasteHTML(string)
}
}
// IE == 11
else if (isIE11 && document.getSelection) {
var range = document.getSelection().getRangeAt(0)
var nnode = document.createElement('div')
range.surroundContents(nnode)
nnode.innerHTML = string
}
else {
document.execCommand('insertHTML', false, string)
}
}
$.fn[pluginName] = function (options) {
return this.each(function () { return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) { if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName, $.data(this, 'plugin_' + pluginName,
@ -537,6 +583,9 @@
// get correct val if textbox // get correct val if textbox
$.fn.ceg = function() { $.fn.ceg = function() {
var plugin = $.data(this[0], 'plugin_' + pluginName) var plugin = $.data(this[0], 'plugin_' + pluginName)
if (!plugin) {
return
}
return plugin.value() return plugin.value()
} }

View file

@ -43,17 +43,21 @@
this.$element.on('keydown', function (e) { this.$element.on('keydown', function (e) {
// esc
if (e.keyCode === 27) {
_this.close()
}
// navigate through item // navigate through item
if (_this.isActive()) { if (_this.isActive()) {
// esc
if (e.keyCode === 27) {
e.preventDefault()
e.stopPropagation()
_this.close()
return
}
// enter // enter
if (e.keyCode === 13) { if (e.keyCode === 13) {
e.preventDefault() e.preventDefault()
e.stopPropagation()
var id = _this.$widget.find('.dropdown-menu li.is-active').data('id') var id = _this.$widget.find('.dropdown-menu li.is-active').data('id')
// as fallback use hovered element // as fallback use hovered element
@ -72,12 +76,14 @@
// arrow keys left/right // arrow keys left/right
if (e.keyCode === 37 || e.keyCode === 39) { if (e.keyCode === 37 || e.keyCode === 39) {
e.preventDefault() e.preventDefault()
e.stopPropagation()
return return
} }
// up or down // up or down
if (e.keyCode === 38 || e.keyCode === 40) { if (e.keyCode === 38 || e.keyCode === 40) {
e.preventDefault() e.preventDefault()
e.stopPropagation()
var active = _this.$widget.find('.dropdown-menu li.is-active') var active = _this.$widget.find('.dropdown-menu li.is-active')
active.removeClass('is-active') active.removeClass('is-active')
@ -92,6 +98,9 @@
var menu = _this.$widget.find('.dropdown-menu') var menu = _this.$widget.find('.dropdown-menu')
if (!active.get(0)) {
return
}
if (active.position().top < 0) { if (active.position().top < 0) {
// scroll up // scroll up
menu.scrollTop( menu.scrollTop() + active.position().top ) menu.scrollTop( menu.scrollTop() + active.position().top )
@ -102,7 +111,11 @@
menu.scrollTop( menu.scrollTop() + invisibleHeight ) menu.scrollTop( menu.scrollTop() + invisibleHeight )
} }
} }
}
// esc
if (e.keyCode === 27) {
_this.close()
} }
}) })
@ -180,14 +193,14 @@
Plugin.prototype.renderBase = function() { Plugin.prototype.renderBase = function() {
this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>') this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>')
this.$widget = this.$element.next() this.$widget = this.$element.next()
this.$widget.on('click', 'li', $.proxy(this.onEntryClick, this)) this.$widget.on('mousedown', 'li', $.proxy(this.onEntryClick, this))
this.$widget.on('mouseenter', 'li', $.proxy(this.onMouseEnter, this)) this.$widget.on('mouseenter', 'li', $.proxy(this.onMouseEnter, this))
} }
// set height of widget // set height of widget
Plugin.prototype.movePosition = function() { Plugin.prototype.movePosition = function() {
if (!this._position) return if (!this._position) return
var height = this.$element.height() + 2 var height = this.$element.outerHeight() + 2
var widgetHeight = this.$widget.find('ul').height() //+ 60 // + height var widgetHeight = this.$widget.find('ul').height() //+ 60 // + height
var top = -( widgetHeight + height ) + this._position.top var top = -( widgetHeight + height ) + this._position.top
var left = this._position.left - 6 var left = this._position.left - 6
@ -250,9 +263,21 @@
// paste some content // paste some content
Plugin.prototype.paste = function(string) { Plugin.prototype.paste = function(string) {
if (document.selection) { // IE var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
// IE <= 10
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange() var range = document.selection.createRange()
range.pasteHTML(string) if (range.pasteHTML) {
range.pasteHTML(string)
}
}
// IE == 11
else if (isIE11 && document.getSelection) {
var range = document.getSelection().getRangeAt(0)
var nnode = document.createElement('div')
range.surroundContents(nnode)
nnode.innerHTML = string
} }
else { else {
document.execCommand('insertHTML', false, string) document.execCommand('insertHTML', false, string)
@ -295,14 +320,7 @@
// for chrome, insert space again // for chrome, insert space again
if (start) { if (start) {
if (spacerChar === ' ') { if (spacerChar === ' ') {
string = "&nbsp;" this.paste('&nbsp;')
if (document.selection) { // IE
var range = document.selection.createRange()
range.pasteHTML(string)
}
else {
document.execCommand('insertHTML', false, string)
}
} }
} }
} }
@ -313,6 +331,7 @@
} }
Plugin.prototype.onEntryClick = function(event) { Plugin.prototype.onEntryClick = function(event) {
event.preventDefault()
var id = $(event.target).data('id') var id = $(event.target).data('id')
this.take(id) this.take(id)
} }
@ -325,7 +344,7 @@
} }
for (var i = 0; i < this.collection.length; i++) { for (var i = 0; i < this.collection.length; i++) {
var item = this.collection[i] var item = this.collection[i]
if ( item.id == id ) { if (item.id == id) {
var content = item.content var content = item.content
this.cutInput() this.cutInput()
this.paste(content) this.paste(content)

View file

@ -10,6 +10,8 @@
modified by Felix Jan-2014 modified by Felix Jan-2014
- add this.$body = $(options.container || document.body) - add this.$body = $(options.container || document.body)
- adjustBackdrop: also adopt left, top and width from $body - adjustBackdrop: also adopt left, top and width from $body
modified by Felix Jul-2017
- add rtl support
*/ */
@ -244,6 +246,10 @@
.css('height', 0) .css('height', 0)
.css('height', this.$element[0].scrollHeight) .css('height', this.$element[0].scrollHeight)
if(App.i18n.dir() == 'rtl'){
this.$backdrop.css('right', 'auto')
}
if(this.scrollbarWidth){ if(this.scrollbarWidth){
this.$backdrop.css('width', this.$body.width() - this.scrollbarWidth) this.$backdrop.css('width', this.$body.width() - this.scrollbarWidth)
} }
@ -251,14 +257,22 @@
Modal.prototype.adjustDialog = function () { Modal.prototype.adjustDialog = function () {
var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
var css = {
this.$element.css({
left: this.$body.offset().left, left: this.$body.offset().left,
top: this.$body.offset().top, top: this.$body.offset().top,
width: this.$body.width(), width: this.$body.width(),
paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '',
paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : ''
}) }
if(App.i18n.dir() == 'rtl'){
css.right = 'auto'
var paddingLeft = css.paddingLeft
css.paddingLeft = css.paddingRight
css.paddingRight = paddingLeft
}
this.$element.css(css)
} }
Modal.prototype.resetAdjustments = function () { Modal.prototype.resetAdjustments = function () {

View file

@ -32,6 +32,10 @@ class App.Model extends Spine.Model
return @title return @title
if @subject if @subject
return @subject return @subject
if @phone
return @phone
if @login
return @login
return '???' return '???'
displayNameLong: -> displayNameLong: ->
@ -57,6 +61,12 @@ class App.Model extends Spine.Model
return @email return @email
if @title if @title
return @title return @title
if @subject
return @subject
if @phone
return @phone
if @login
return @login
return '???' return '???'
icon: (user) -> icon: (user) ->
@ -165,6 +175,31 @@ class App.Model extends Spine.Model
### ###
set new attributes of model (remove already available attributes)
App.Model.attributesSet(attributes)
###
@attributesSet: (attributes) ->
configure_attributes = App[ @.className ].configure_attributes
attributesNew = []
for localAttribute in configure_attributes
found = false
for attribute in attributes
if attribute.name is localAttribute.name
found = true
break
if !found
attributesNew.push localAttribute
for attribute in attributes
App[@.className].attributes.push attribute.name
attributesNew.push attribute
App[ @.className ].configure_attributes = attributesNew
###
attributes = App.Model.attributesGet(optionalScreen, optionalAttributesList) attributes = App.Model.attributesGet(optionalScreen, optionalAttributesList)
returns returns

View file

@ -34,4 +34,12 @@ class App.Group extends App.Model
cssClass.push("avatar--group-color-#{@id % 3}") cssClass.push("avatar--group-color-#{@id % 3}")
return App.view('avatar_group') return App.view('avatar_group')
cssClass: cssClass.join(' ') cssClass: cssClass.join(' ')
@accesses: ->
read: 'Read'
create: 'Create'
change: 'Change'
delete: 'Delete'
overview: 'Overview'
full: 'Full'

View file

@ -6,7 +6,7 @@ class App.Job extends App.Model
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'timeplan', display: 'When should the job run?', tag: 'timer', null: true }, { name: 'timeplan', display: 'When should the job run?', tag: 'timer', null: true },
{ name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: true }, { name: 'condition', display: 'Conditions for effected objects', tag: 'ticket_selector', null: true },
{ name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true }, { name: 'perform', display: 'Execute changes on objects', tag: 'ticket_perform_action', null: true, notification: true, ticket_delete: true },
{ name: 'disable_notification', display: 'Disable Notifications', tag: 'boolean', default: true }, { name: 'disable_notification', display: 'Disable Notifications', tag: 'boolean', default: true },
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true }, { name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
{ name: 'active', display: 'Active', tag: 'active', default: true }, { name: 'active', display: 'Active', tag: 'active', default: true },

View file

@ -8,6 +8,7 @@ class App.Overview extends App.Model
{ name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, null: false, relation: 'Role', translate: true }, { name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, null: false, relation: 'Role', translate: true },
{ name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, null: true, relation: 'User', sortBy: 'firstname' }, { name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, null: true, relation: 'User', sortBy: 'firstname' },
{ name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true }, { name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true },
{ name: 'out_of_office', display: 'Only available for Users which are replacements for other users.', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true },
{ name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false }, { name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false },
{ name: 'prio', display: 'Prio', readonly: 1 }, { name: 'prio', display: 'Prio', readonly: 1 },
{ {
@ -72,4 +73,4 @@ Sie können auch individuelle Übersichten für einzelne Agenten oder agenten Gr
''' '''
uiUrl: -> uiUrl: ->
'#ticket/view/' + @link "#ticket/view/#{@link}"

View file

@ -6,7 +6,7 @@ class App.PostmasterFilter extends App.Model
@configure_attributes = [ @configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 250, 'null': false }, { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 250, 'null': false },
{ name: 'channel', display: 'Channel', type: 'input', readonly: 1 }, { name: 'channel', display: 'Channel', type: 'input', readonly: 1 },
{ name: 'match', display: 'Match all of the following', tag: 'postmaster_match' }, { name: 'match', display: 'Match all of the following', tag: 'postmaster_match', note: 'You can use regular expression by using "regex:your_reg_exp".' },
{ name: 'perform', display: 'Perform action of the following', tag: 'postmaster_set' }, { name: 'perform', display: 'Perform action of the following', tag: 'postmaster_set' },
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, { name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },

View file

@ -1,5 +1,5 @@
class App.Role extends App.Model class App.Role extends App.Model
@configure 'Role', 'name', 'permission_ids', 'default_at_signup', 'note', 'active', 'updated_at' @configure 'Role', 'name', 'permission_ids', 'group_ids', 'default_at_signup', 'note', 'active', 'updated_at'
@extend Spine.Model.Ajax @extend Spine.Model.Ajax
@url: @apiPath + '/roles' @url: @apiPath + '/roles'
@configure_attributes = [ @configure_attributes = [

View file

@ -53,8 +53,14 @@ class App.User extends App.Model
cssClass += ' ' if cssClass cssClass += ' ' if cssClass
cssClass += "size-#{ size }" cssClass += "size-#{ size }"
if @active is false
cssClass += ' avatar--inactive'
if @isOutOfOffice()
cssClass += ' avatar--vacation'
if placement if placement
placement = " data-placement='#{ placement }'" placement = " data-placement='#{placement}'"
if !avatar if !avatar
if type is 'personal' if type is 'personal'
@ -104,6 +110,19 @@ class App.User extends App.Model
vip: vip vip: vip
url: @imageUrl() url: @imageUrl()
isOutOfOffice: ->
return false if @out_of_office isnt true
start_time = @out_of_office_start_at
return false if !start_time
end_time = @out_of_office_end_at
return false if !end_time
start_time = new Date(Date.parse(start_time))
end_time = new Date(Date.parse(end_time))
now = new Date((new Date).toDateString())
if start_time <= now && end_time >= now
return true
false
imageUrl: -> imageUrl: ->
return if !@image return if !@image
# set image url # set image url
@ -237,3 +256,16 @@ class App.User extends App.Model
break break
return access if access return access if access
false false
@outOfOfficeTextPlaceholder: ->
today = new Date()
outOfOfficeText = 'Christmas holiday'
if today.getMonth() < 3
outOfOfficeText = 'Easter holiday'
else if today.getMonth() < 9
outOfOfficeText = 'Summer holiday'
outOfOfficeText
outOfOfficeText: ->
return @preferences.out_of_office_text if !_.isEmpty(@preferences.out_of_office_text)
App.User.outOfOfficeTextPlaceholder()

View file

@ -142,7 +142,7 @@
<h3><%- @T('Automatically show chat') %> (<%- @T('default') %>)</h3> <h3><%- @T('Automatically show chat') %> (<%- @T('default') %>)</h3>
<p><%- @T('The chat will show up once the connection to the server got established and if there is someone online to chat with.') %></p> <p><%- @T('The chat will show up once the connection to the server got established and if there is someone online to chat with.') %></p>
<pre><code class="language-html js-paramsBlock">&lt;script src="<%= @baseurl %>/assets/chat/chat.min.js"&gt;&lt;/script&gt; <pre><code class="language-html js-code">&lt;script src="<%= @baseurl %>/assets/chat/chat.min.js"&gt;&lt;/script&gt;
&lt;script&gt; &lt;script&gt;
$(function() { $(function() {
new ZammadChat({ new ZammadChat({
@ -153,7 +153,7 @@ $(function() {
<h3><%- @T('Manually open chat') %></h3> <h3><%- @T('Manually open chat') %></h3>
<p><%- @T('If you want to open the chat by the press of a button set the option §show§ to §false§ and add the class §open-zammad-chat§ to the button.') %></p> <p><%- @T('If you want to open the chat by the press of a button set the option §show§ to §false§ and add the class §open-zammad-chat§ to the button.') %></p>
<pre><code class="language-html js-paramsBlock">&lt;button class="open-zammad-chat"&gt;Chat with us&lt;/button&gt; <pre><code class="language-html js-code">&lt;button class="open-zammad-chat"&gt;Chat with us&lt;/button&gt;
&lt;script src="<%= @baseurl %>/assets/chat/chat.min.js"&gt;&lt;/script&gt; &lt;script src="<%= @baseurl %>/assets/chat/chat.min.js"&gt;&lt;/script&gt;
&lt;script&gt; &lt;script&gt;

View file

@ -10,8 +10,20 @@
<div class="page-content"> <div class="page-content">
<p><%- @T('With form you can add a form to your web page which directly generates a ticket for you.') %></p> <p><%- @T('With form you can add a form to your web page which directly generates a ticket for you.') %></p>
<h2><%- @T('Settings') %></h2>
<form class="js-paramsSetting">
<fieldset>
<div class="input form-group formGroup--halfSize">
<div class="formGroup-label">
<label for="form-group"><%- @T('Group selection for Ticket creation') %></label>
</div>
<div class="controls js-groupSelector" id="from-group"></div>
</div>
</fieldset>
</form>
<h2><%- @T('Designer') %></h2> <h2><%- @T('Designer') %></h2>
<form class="js-params"> <form class="js-paramsDesigner">
<fieldset> <fieldset>
<div class="input form-group formGroup--halfSize"> <div class="input form-group formGroup--halfSize">
@ -114,6 +126,9 @@
</div> </div>
</div> </div>
</div> </div>
<h3><%- @T('Requirements') %></h3>
<p><%- @T("Zammad Forms requires jQuery. If you don't already use it on your website include it like this:") %></p>
<pre><code class="language-html js-code">&lt;script src="https://code.jquery.com/jquery-2.1.4.min.js"&gt;&lt;/script&gt;</code></pre>
<p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p> <p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p>

View file

@ -26,6 +26,8 @@
<div class="chat-body js-body"></div> <div class="chat-body js-body"></div>
</div> </div>
<div class="chat-controls"> <div class="chat-controls">
<div class="chat-input form-control form-control--small form-control--multiline js-customerChatInput" contenteditable="true"></div> <div class="chat-input">
<div class="form-control form-control--small form-control--multiline js-customerChatInput" contenteditable="true"></div>
</div>
<div class="btn btn--primary btn--slim btn--small js-send"><%- @T('Send') %></div> <div class="btn btn--primary btn--slim btn--small js-send"><%- @T('Send') %></div>
</div> </div>

View file

@ -9,7 +9,7 @@
<label for="application_id">Facebook APP ID <span>*</span></label> <label for="application_id">Facebook APP ID <span>*</span></label>
</div> </div>
<div class="controls"> <div class="controls">
<input id="application_id" type="text" name="application_id" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.application_id %><% end %>" class="form-control" required autocomplete="new-password"> <input id="application_id" type="text" name="application_id" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.application_id %><% end %>" class="form-control" required autocomplete="off">
</div> </div>
</div> </div>
<div class="input form-group"> <div class="input form-group">
@ -17,7 +17,7 @@
<label for="application_secret">Facebook App Secret <span>*</span></label> <label for="application_secret">Facebook App Secret <span>*</span></label>
</div> </div>
<div class="controls"> <div class="controls">
<input id="application_secret" type="text" name="application_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.application_secret %><% end %>" class="form-control" required autocomplete="new-password"> <input id="application_secret" type="text" name="application_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.application_secret %><% end %>" class="form-control" required autocomplete="off">
</div> </div>
</div> </div>
<h2><%- @T('Your callback URL') %></h2> <h2><%- @T('Your callback URL') %></h2>

View file

@ -1,3 +1,3 @@
<input id="<%= @attribute.id %>" type="hidden" name="<%= @attribute.name %>" value="<%= @attribute.value %>" <%= @attribute.required %> /> <input id="<%= @attribute.id %>" type="hidden" name="<%= @attribute.name %>" value="<%= @attribute.value %>" <%= @attribute.required %> />
<input id="<%= @attribute.id %>_autocompletion" type="text" name="<%= @attribute.name %>_autocompletion" value="<%= @attribute.valueShown %>" class="form-control <%= @attribute.class %>" <%= @attribute.required %> <%= @attribute.autofocus %> <%- @attribute.autocapitalize %> <% if @attribute.placeholder: %>placeholder="<%- @Ti(@attribute.placeholder) %>"<% end %> autocomplete="new-password"/> <input id="<%= @attribute.id %>_autocompletion" type="text" name="<%= @attribute.name %>_autocompletion" value="<%= @attribute.valueShown %>" class="form-control <%= @attribute.class %>" <%= @attribute.required %> <%= @attribute.autofocus %> <%- @attribute.autocapitalize %> <% if @attribute.placeholder: %>placeholder="<%- @Ti(@attribute.placeholder) %>"<% end %> autocomplete="off"/>
<input id="<%= @attribute.id %>_autocompletion_value_shown" type="hidden" name="<%= @attribute.name %>_autocompletion_value_shown" value="<%= @attribute.valueShown %>"/> <input id="<%= @attribute.id %>_autocompletion_value_shown" type="hidden" name="<%= @attribute.name %>_autocompletion_value_shown" value="<%= @attribute.valueShown %>"/>

View file

@ -4,7 +4,7 @@
<input type="checkbox" value="<%= row.value %>" name="<%= @attribute.name %>" <%= row.checked %>/> <input type="checkbox" value="<%= row.value %>" name="<%= @attribute.name %>" <%= row.checked %>/>
<%- @Icon('checkbox', 'icon-unchecked') %> <%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %> <%- @Icon('checkbox-checked', 'icon-checked') %>
<span class="label-text"><%= row.name %> <% if row.note: %>- <span class="help-text"><%= row.note %></span><% end %></span> <span class="label-text"><%= row.name %> <% if row.note: %>- <span class="help-text"><%- @T(row.note) %></span><% end %></span>
</label> </label>
<% end %> <% end %>
</div> </div>

View file

@ -3,7 +3,7 @@
<% if @attribute.multiple: %> <% if @attribute.multiple: %>
<%- @tokens %> <%- @tokens %>
<% end %> <% end %>
<input name="<%- @attribute.name %>_completion" class="user-select token-input js-objectSelect" autocapitalize="off" placeholder="<%- @attribute.placeholder %>" autocomplete="new-password" <%= @attribute.autofocus %> role="textbox" aria-autocomplete="list" value="<%= @name %>" aria-haspopup="true"> <input name="<%- @attribute.name %>_completion" class="user-select token-input js-objectSelect" autocapitalize="off" placeholder="<%- @attribute.placeholder %>" autocomplete="off" <%= @attribute.autofocus %> role="textbox" aria-autocomplete="list" value="<%= @name %>" aria-haspopup="true">
<% if @attribute.disableCreateObject isnt true: %><%- @Icon('arrow-down', 'dropdown-arrow') %><% end %> <% if @attribute.disableCreateObject isnt true: %><%- @Icon('arrow-down', 'dropdown-arrow') %><% end %>
</div> </div>

View file

@ -15,6 +15,36 @@
<%- @Icon('checkbox-checked', 'icon-checked') %> <%- @Icon('checkbox-checked', 'icon-checked') %>
<span class="label-text"><%= permission.displayName().replace(/^.+?\./, '') %> - <span class="help-text"><%- @T.apply(@, [permission.note].concat(permission.preferences.translations)) %></span></span> <span class="label-text"><%= permission.displayName().replace(/^.+?\./, '') %> - <span class="help-text"><%- @T.apply(@, [permission.note].concat(permission.preferences.translations)) %></span></span>
</label> </label>
<% if _.contains(permission.preferences.plugin, 'groups'): %>
<div style="padding-left: 18px; padding-top: 10px;" class="js-groupList <% if @hideGroups: %>js-groupListHide hidden<% end %>">
<table class="settings-list">
<thead>
<th><%- @T('Group') %>
<% for key, text of @groupAccesses: %>
<th><%- @T(text) %>
<% end %>
<tbody>
<% for group in @groups: %>
<% accesses = [] %>
<% if @params.group_ids && @params.group_ids[group.id]: %>
<% accesses = @params.group_ids[group.id] %>
<% end %>
<tr>
<td>
<%= group.displayName() %>
<% for key, text of @groupAccesses: %>
<td>
<label class="inline-label checkbox-replacement">
<input type="checkbox" value="<%= key %>" name="group_ids::<%= group.id %>" <% if _.contains(accesses, key): %>checked<% end %>/>
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
</label>
<% end %>
</tr>
<% end %>
</table>
</div>
<% end %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>

View file

@ -1,7 +1,7 @@
<div class="<%= @attribute.class %>"> <div class="<%= @attribute.class %>">
<% for row in @attribute.options: %> <% for row in @attribute.options: %>
<label> <label>
<input type="radio" value="<%= row.value %>" name="<%= @attribute.name %>" <%= row.checked %>> <%= row.name %> <% if row.note: %>- <%= row.note %><% end %> <input type="radio" value="<%= row.value %>" name="<%= @attribute.name %>" <%= row.checked %>> <%= row.name %> <% if row.note: %>- <%- @T(row.note) %><% end %>
<%- @Icon('radio') %> <%- @Icon('radio') %>
<%- @Icon('radio-checked') %> <%- @Icon('radio-checked') %>
</label> </label>

View file

@ -1,25 +1,29 @@
<div class="dropdown-toggle" data-toggle="dropdown"> <div class="dropdown-toggle" data-toggle="dropdown">
<input <input
class="searchableSelect-shadow form-control js-shadow" class="searchableSelect-shadow form-control js-shadow"
id="<%= @id %>" id="<%= @attribute.id %>"
name="<%= @name %>" name="<%= @attribute.name %>"
<%= @required %> <%= @attribute.required %>
<%= @autofocus %> <%= @attribute.autofocus %>
value="<%= @value %>" value="<%= @attribute.value %>"
> >
<input <input
class="searchableSelect-main form-control js-input<%= " #{ @class }" if @class %>" class="searchableSelect-main form-control js-input<%= " #{ @attribute.class }" if @attribute.class %>"
placeholder="<%= @placeholder %>" placeholder="<%= @attribute.placeholder %>"
value="<%= @valueName %>" value="<%= @attribute.valueName %>"
autocomplete="new-password" autocomplete="off"
<%= @attribute.required %>
> >
<div class="searchableSelect-autocomplete"> <div class="searchableSelect-autocomplete">
<span class="searchableSelect-autocomplete-invisible js-autocomplete-invisible"></span> <span class="searchableSelect-autocomplete-invisible js-autocomplete-invisible"></span>
<span class="searchableSelect-autocomplete-visible js-autocomplete-visible"></span> <span class="searchableSelect-autocomplete-visible js-autocomplete-visible"></span>
</div> </div>
<% if !@ajax: %><%- @Icon('arrow-down', 'dropdown-arrow') %><% end %> <% if !@attribute.ajax: %><%- @Icon('arrow-down', 'dropdown-arrow') %><% end %>
<div class="small loading icon"></div> <div class="small loading icon"></div>
</div> </div>
<ul class="dropdown-menu dropdown-menu-left js-optionsList" role="menu"> <div class="dropdown-menu dropdown-menu-left dropdown-menu--has-submenu js-dropdown">
<%- @renderedOptions %> <ul class="js-optionsList" role="menu">
</ul> <%- @options %>
</ul>
<%- @submenus %>
</div>

View file

@ -0,0 +1,8 @@
<li role="presentation" class="<%= @class %>" data-value="<%= @option.value %>" title="<%= @option.name %><% if @detail: %><%= @detail %><% end %>">
<span class="searchableSelect-option-text">
<%= @option.name %><% if @detail: %><span class="dropdown-detail"><%= @detail %></span><% end %>
</span>
<% if @option.children: %>
<%- @Icon('arrow-right', 'recipientList-arrow') %>
<% end %>
</li>

View file

@ -1,5 +0,0 @@
<% if @options: %>
<% for option in @options: %>
<li role="presentation" class="js-option" data-value="<%= option.value %>"><%= option.name %>
<% end %>
<% end %>

Some files were not shown because too many files have changed in this diff Show more