Merge branch 'develop' into private-pull-request-1118
This commit is contained in:
commit
6ab397ef44
573 changed files with 25280 additions and 5097 deletions
|
@ -3,7 +3,7 @@ before_script:
|
|||
- which ruby
|
||||
- env
|
||||
- test -n "$RNAME" && script/build/test_db_config.sh
|
||||
- test -n "$RNAME" && bundle install
|
||||
- test -n "$RNAME" && bundle install --jobs 8
|
||||
|
||||
stages:
|
||||
- pre
|
||||
|
@ -127,6 +127,17 @@ test:integration:email_deliver:
|
|||
- ruby -I test/ test/integration/email_deliver_test.rb
|
||||
- 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:
|
||||
stage: test
|
||||
tags:
|
||||
|
@ -229,7 +240,7 @@ test:integration:slack:
|
|||
- rake db:create
|
||||
- rake db:migrate
|
||||
- echo "gem 'slack-api'" >> Gemfile.local
|
||||
- bundle install
|
||||
- bundle install --jobs 8
|
||||
- ruby -I test test/integration/slack_test.rb
|
||||
- rake db:drop
|
||||
|
||||
|
@ -281,6 +292,7 @@ test:integration:es_mysql:
|
|||
- ruby -I test/ test/integration/elasticsearch_test.rb
|
||||
- ruby -I test/ test/controllers/search_controller_test.rb
|
||||
- ruby -I test/ test/integration/report_test.rb
|
||||
- ruby -I test/ test/controllers/form_controller_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:es_postgresql:
|
||||
|
@ -297,6 +309,7 @@ test:integration:es_postgresql:
|
|||
- ruby -I test/ test/integration/elasticsearch_test.rb
|
||||
- ruby -I test/ test/controllers/search_controller_test.rb
|
||||
- ruby -I test/ test/integration/report_test.rb
|
||||
- ruby -I test/ test/controllers/form_controller_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:zendesk_mysql:
|
||||
|
@ -330,7 +343,7 @@ test:integration:otrs_5_mysql:
|
|||
- mysql
|
||||
script:
|
||||
- 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:migrate
|
||||
- ruby -I test/ test/integration/otrs_import_test.rb
|
||||
|
@ -343,7 +356,7 @@ test:integration:otrs_5_postgresql:
|
|||
- postgresql
|
||||
script:
|
||||
- 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:migrate
|
||||
- ruby -I test/ test/integration/otrs_import_test.rb
|
||||
|
@ -409,6 +422,7 @@ browser:build:
|
|||
- rake assets:precompile
|
||||
- rake db:drop
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public/assets/.sprockets-manifest*
|
||||
- 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
|
||||
- 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
|
||||
- bundle install
|
||||
- bundle install --jobs 8
|
||||
- export TEST_URL=http://$IP:$BROWSER_PORT
|
||||
- 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
|
||||
|
|
|
@ -5,19 +5,23 @@ notifications: false
|
|||
targets:
|
||||
centos-7:
|
||||
dependencies:
|
||||
- elasticsearch
|
||||
- nginx
|
||||
- postgresql-server
|
||||
- which
|
||||
debian-8:
|
||||
dependencies:
|
||||
- elasticsearch
|
||||
- nginx|apache2
|
||||
- postgresql|mysql-server|mariadb-server|sqlite
|
||||
ubuntu-16.04:
|
||||
dependencies:
|
||||
- elasticsearch
|
||||
- nginx|apache2
|
||||
- postgresql|mysql-server|mariadb-server|sqlite
|
||||
sles-12:
|
||||
dependencies:
|
||||
- elasticsearch
|
||||
- nginx
|
||||
- postgresql-server
|
||||
before:
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.3.1
|
||||
2.4.1
|
||||
|
|
|
@ -19,7 +19,7 @@ services:
|
|||
- mysql
|
||||
language: ruby
|
||||
rvm:
|
||||
- 2.3.1
|
||||
- 2.4.1
|
||||
before_install:
|
||||
- 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
|
||||
|
@ -55,3 +55,4 @@ script:
|
|||
- ruby -I test/ test/integration/user_device_controller_test.rb
|
||||
- ruby -I test/ test/integration/sipgate_controller_test.rb
|
||||
- rake db:drop
|
||||
after_success: contrib/travis-ci.org/trigger-docker-compose-build.sh
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Change Log
|
||||
|
||||
## [1.6.0](https://github.com/zammad/zammad/tree/1.6.0) (2017-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/1.4.0...1.5.0)
|
||||
## [2.1.0](https://github.com/zammad/zammad/tree/2.1.0) (2017-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/2.0.0...2.1.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
|
|
12
Gemfile
12
Gemfile
|
@ -1,8 +1,8 @@
|
|||
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 'activerecord-session_store'
|
||||
|
||||
|
@ -40,18 +40,20 @@ gem 'omniauth-gitlab'
|
|||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-linkedin-oauth2'
|
||||
gem 'omniauth-twitter'
|
||||
gem 'omniauth-microsoft-office365'
|
||||
|
||||
gem 'twitter'
|
||||
gem 'telegramAPI'
|
||||
gem 'koala'
|
||||
gem 'mail'
|
||||
gem 'email_verifier'
|
||||
gem 'valid_email2'
|
||||
gem 'htmlentities'
|
||||
|
||||
gem 'mime-types'
|
||||
|
||||
gem 'biz'
|
||||
|
||||
gem 'composite_primary_keys'
|
||||
gem 'delayed_job_active_record'
|
||||
gem 'daemons'
|
||||
|
||||
|
@ -72,12 +74,16 @@ gem 'argon2'
|
|||
|
||||
gem 'writeexcel'
|
||||
gem 'icalendar'
|
||||
gem 'icalendar-recurrence'
|
||||
gem 'browser'
|
||||
|
||||
# integrations
|
||||
gem 'slack-notifier'
|
||||
gem 'clearbit'
|
||||
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
|
||||
gem 'eventmachine'
|
||||
|
|
272
Gemfile.lock
272
Gemfile.lock
|
@ -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
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actionmailer (4.2.7.1)
|
||||
actionpack (= 4.2.7.1)
|
||||
actionview (= 4.2.7.1)
|
||||
activejob (= 4.2.7.1)
|
||||
actionmailer (4.2.9)
|
||||
actionpack (= 4.2.9)
|
||||
actionview (= 4.2.9)
|
||||
activejob (= 4.2.9)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
actionpack (4.2.7.1)
|
||||
actionview (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
actionpack (4.2.9)
|
||||
actionview (= 4.2.9)
|
||||
activesupport (= 4.2.9)
|
||||
rack (~> 1.6)
|
||||
rack-test (~> 0.6.2)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
actionview (4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
actionview (4.2.9)
|
||||
activesupport (= 4.2.9)
|
||||
builder (~> 3.1)
|
||||
erubis (~> 2.7.0)
|
||||
rails-dom-testing (~> 1.0, >= 1.0.5)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
||||
activejob (4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
||||
activejob (4.2.9)
|
||||
activesupport (= 4.2.9)
|
||||
globalid (>= 0.3.0)
|
||||
activemodel (4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
activemodel (4.2.9)
|
||||
activesupport (= 4.2.9)
|
||||
builder (~> 3.1)
|
||||
activerecord (4.2.7.1)
|
||||
activemodel (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
activerecord (4.2.9)
|
||||
activemodel (= 4.2.9)
|
||||
activesupport (= 4.2.9)
|
||||
arel (~> 6.0)
|
||||
activerecord-nulldb-adapter (0.3.6)
|
||||
activerecord-nulldb-adapter (0.3.7)
|
||||
activerecord (>= 2.0.0)
|
||||
activerecord-session_store (1.0.0)
|
||||
actionpack (>= 4.0, < 5.1)
|
||||
activerecord (>= 4.0, < 5.1)
|
||||
activerecord-session_store (1.1.0)
|
||||
actionpack (>= 4.0, < 5.2)
|
||||
activerecord (>= 4.0, < 5.2)
|
||||
multi_json (~> 1.11, >= 1.11.2)
|
||||
rack (>= 1.5.2, < 3)
|
||||
railties (>= 4.0, < 5.1)
|
||||
activesupport (4.2.7.1)
|
||||
railties (>= 4.0, < 5.2)
|
||||
activesupport (4.2.9)
|
||||
i18n (~> 0.7)
|
||||
json (~> 1.7, >= 1.7.7)
|
||||
minitest (~> 5.1)
|
||||
thread_safe (~> 0.3, >= 0.3.4)
|
||||
tzinfo (~> 1.1)
|
||||
addressable (2.4.0)
|
||||
arel (6.0.3)
|
||||
arel (6.0.4)
|
||||
argon2 (1.1.3)
|
||||
ffi (~> 1.9)
|
||||
ffi-compiler (~> 0.1)
|
||||
ast (2.3.0)
|
||||
autoprefixer-rails (6.4.1.1)
|
||||
autoprefixer-rails (7.1.2.4)
|
||||
execjs
|
||||
biz (1.6.0)
|
||||
biz (1.7.0)
|
||||
clavius (~> 1.0)
|
||||
tzinfo
|
||||
browser (2.2.0)
|
||||
buftok (0.2.0)
|
||||
builder (3.2.2)
|
||||
builder (3.2.3)
|
||||
childprocess (0.5.9)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
clavius (1.0.2)
|
||||
clearbit (0.2.5)
|
||||
clearbit (0.2.7)
|
||||
nestful (~> 1.1.0)
|
||||
coderay (1.1.1)
|
||||
coffee-rails (4.2.1)
|
||||
|
@ -70,31 +85,32 @@ GEM
|
|||
coffee-script (2.4.1)
|
||||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.10.0)
|
||||
coffee-script-source (1.12.2)
|
||||
coffeelint (1.14.0)
|
||||
coffee-script
|
||||
execjs
|
||||
json
|
||||
concurrent-ruby (1.0.2)
|
||||
coveralls (0.8.16)
|
||||
composite_primary_keys (8.1.6)
|
||||
activerecord (~> 4.2.0)
|
||||
concurrent-ruby (1.0.5)
|
||||
coveralls (0.8.21)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov (~> 0.12.0)
|
||||
term-ansicolor (~> 1.3.0)
|
||||
thor (~> 0.19.1)
|
||||
tins (>= 1.6.0, < 2)
|
||||
simplecov (~> 0.14.1)
|
||||
term-ansicolor (~> 1.3)
|
||||
thor (~> 0.19.4)
|
||||
tins (~> 1.6)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
daemons (1.2.4)
|
||||
delayed_job (4.1.2)
|
||||
activesupport (>= 3.0, < 5.1)
|
||||
delayed_job_active_record (4.1.1)
|
||||
activerecord (>= 3.0, < 5.1)
|
||||
delayed_job (4.1.3)
|
||||
activesupport (>= 3.0, < 5.2)
|
||||
delayed_job_active_record (4.1.2)
|
||||
activerecord (>= 3.0, < 5.2)
|
||||
delayed_job (>= 3.0, < 5)
|
||||
diff-lcs (1.2.5)
|
||||
diffy (3.1.0)
|
||||
dnsruby (1.59.3)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.20160826)
|
||||
domain_name (0.5.20170404)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (4.2.0)
|
||||
railties (>= 4.2)
|
||||
|
@ -106,8 +122,6 @@ GEM
|
|||
em-websocket (0.5.1)
|
||||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
email_verifier (0.1.0)
|
||||
dnsruby (>= 1.5)
|
||||
equalizer (0.0.10)
|
||||
erubis (2.7.0)
|
||||
eventmachine (1.2.3)
|
||||
|
@ -136,8 +150,8 @@ GEM
|
|||
rainbow (>= 2.1)
|
||||
rake (>= 10.0)
|
||||
retriable (~> 2.1)
|
||||
globalid (0.3.7)
|
||||
activesupport (>= 4.1.0)
|
||||
globalid (0.4.0)
|
||||
activesupport (>= 4.2.0)
|
||||
guard (2.14.0)
|
||||
formatador (>= 0.2.4)
|
||||
listen (>= 2.7, < 4.0)
|
||||
|
@ -153,39 +167,49 @@ GEM
|
|||
guard (~> 2.8)
|
||||
guard-compat (~> 1.0)
|
||||
multi_json (~> 1.8)
|
||||
guard-symlink (0.1.0)
|
||||
guard-symlink (0.1.1)
|
||||
guard
|
||||
guard-compat (~> 1.1)
|
||||
hashdiff (0.3.2)
|
||||
hashie (3.4.4)
|
||||
hashdiff (0.3.5)
|
||||
hashie (3.5.6)
|
||||
htmlentities (4.3.4)
|
||||
http (1.0.4)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 1.0.1)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
http-cookie (1.0.2)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (1.0.1)
|
||||
http-form_data (1.0.3)
|
||||
http_parser.rb (0.6.0)
|
||||
i18n (0.8.1)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.8.6)
|
||||
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)
|
||||
json (1.8.6)
|
||||
jwt (1.5.4)
|
||||
jwt (1.5.6)
|
||||
kgio (2.11.0)
|
||||
koala (2.4.0)
|
||||
addressable
|
||||
faraday
|
||||
multi_json (>= 1.3.0)
|
||||
libv8 (3.16.14.15)
|
||||
libv8 (3.16.14.19)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
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)
|
||||
nokogiri (>= 1.5.9)
|
||||
lumberjack (1.0.10)
|
||||
mail (2.6.4)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memoizable (0.4.2)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
|
@ -193,50 +217,54 @@ GEM
|
|||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_portile2 (2.1.0)
|
||||
minitest (5.10.1)
|
||||
mini_portile2 (2.2.0)
|
||||
minitest (5.10.3)
|
||||
multi_json (1.12.1)
|
||||
multi_xml (0.5.5)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mysql2 (0.4.4)
|
||||
mysql2 (0.4.6)
|
||||
naught (1.1.0)
|
||||
nenv (0.3.0)
|
||||
nestful (1.1.1)
|
||||
net-ldap (0.15.0)
|
||||
netrc (0.11.0)
|
||||
nokogiri (1.7.1)
|
||||
mini_portile2 (~> 2.1.0)
|
||||
nokogiri (1.8.0)
|
||||
mini_portile2 (~> 2.2.0)
|
||||
nori (2.6.0)
|
||||
notiffany (0.1.1)
|
||||
nenv (~> 0.1)
|
||||
shellany (~> 0.0)
|
||||
oauth (0.5.1)
|
||||
oauth2 (1.2.0)
|
||||
faraday (>= 0.8, < 0.10)
|
||||
oauth2 (1.4.0)
|
||||
faraday (>= 0.8, < 0.13)
|
||||
jwt (~> 1.0)
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
octokit (4.4.1)
|
||||
sawyer (~> 0.7.0, >= 0.5.3)
|
||||
omniauth (1.3.1)
|
||||
hashie (>= 1.2, < 4)
|
||||
rack (>= 1.0, < 3)
|
||||
omniauth (1.6.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-facebook (4.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github (1.1.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-github (1.3.0)
|
||||
omniauth (~> 1.5)
|
||||
omniauth-oauth2 (>= 1.4.0, < 2.0)
|
||||
omniauth-gitlab (1.0.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.0)
|
||||
omniauth-google-oauth2 (0.4.1)
|
||||
jwt (~> 1.5.2)
|
||||
omniauth-google-oauth2 (0.5.0)
|
||||
jwt (~> 1.5)
|
||||
multi_json (~> 1.3)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.3.1)
|
||||
omniauth-linkedin-oauth2 (0.1.5)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2
|
||||
omniauth-microsoft-office365 (0.0.7)
|
||||
omniauth
|
||||
omniauth-oauth2
|
||||
omniauth-oauth (1.1.0)
|
||||
oauth
|
||||
omniauth (~> 1.0)
|
||||
|
@ -248,32 +276,32 @@ GEM
|
|||
omniauth-oauth (~> 1.1)
|
||||
parser (2.3.1.2)
|
||||
ast (~> 2.2)
|
||||
pg (0.18.4)
|
||||
pluginator (1.3.0)
|
||||
pg (0.20.0)
|
||||
pluginator (1.5.0)
|
||||
power_assert (0.3.1)
|
||||
powerpack (0.1.1)
|
||||
pre-commit (0.28.0)
|
||||
pluginator (~> 1.1)
|
||||
pre-commit (0.35.0)
|
||||
pluginator (~> 1.5)
|
||||
pry (0.10.4)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
puma (3.6.0)
|
||||
rack (1.6.4)
|
||||
puma (3.9.1)
|
||||
rack (1.6.8)
|
||||
rack-livereload (0.3.16)
|
||||
rack
|
||||
rack-test (0.6.3)
|
||||
rack (>= 1.0)
|
||||
rails (4.2.7.1)
|
||||
actionmailer (= 4.2.7.1)
|
||||
actionpack (= 4.2.7.1)
|
||||
actionview (= 4.2.7.1)
|
||||
activejob (= 4.2.7.1)
|
||||
activemodel (= 4.2.7.1)
|
||||
activerecord (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
rails (4.2.9)
|
||||
actionmailer (= 4.2.9)
|
||||
actionpack (= 4.2.9)
|
||||
actionview (= 4.2.9)
|
||||
activejob (= 4.2.9)
|
||||
activemodel (= 4.2.9)
|
||||
activerecord (= 4.2.9)
|
||||
activesupport (= 4.2.9)
|
||||
bundler (>= 1.3.0, < 2.0)
|
||||
railties (= 4.2.7.1)
|
||||
railties (= 4.2.9)
|
||||
sprockets-rails
|
||||
rails-deprecated_sanitizer (1.0.3)
|
||||
activesupport (>= 4.2.0.alpha)
|
||||
|
@ -283,15 +311,16 @@ GEM
|
|||
rails-deprecated_sanitizer (>= 1.0.1)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-observers (0.1.2)
|
||||
activemodel (~> 4.0)
|
||||
railties (4.2.7.1)
|
||||
actionpack (= 4.2.7.1)
|
||||
activesupport (= 4.2.7.1)
|
||||
rails-observers (0.1.5)
|
||||
activemodel (>= 4.0)
|
||||
railties (4.2.9)
|
||||
actionpack (= 4.2.9)
|
||||
activesupport (= 4.2.9)
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.18.1, < 2.0)
|
||||
rainbow (2.1.0)
|
||||
raindrops (0.17.0)
|
||||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.19.0)
|
||||
rake (12.0.0)
|
||||
rb-fsevent (0.9.7)
|
||||
rb-inotify (0.9.7)
|
||||
|
@ -339,7 +368,6 @@ GEM
|
|||
sawyer (0.7.0)
|
||||
addressable (>= 2.3.5, < 2.5)
|
||||
faraday (~> 0.8, < 0.10)
|
||||
scrub_rb (1.0.1)
|
||||
selenium-webdriver (2.53.4)
|
||||
childprocess (~> 0.5)
|
||||
rubyzip (~> 1.0)
|
||||
|
@ -347,19 +375,20 @@ GEM
|
|||
shellany (0.0.1)
|
||||
simple-rss (1.3.1)
|
||||
simple_oauth (0.3.1)
|
||||
simplecov (0.12.0)
|
||||
simplecov (0.14.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.0)
|
||||
simplecov-html (0.10.1)
|
||||
simplecov-rcov (0.2.3)
|
||||
simplecov (>= 0.4.1)
|
||||
slack-notifier (1.5.1)
|
||||
slop (3.6.0)
|
||||
spring (1.7.2)
|
||||
spring (2.0.2)
|
||||
activesupport (>= 4.2)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
sprockets (3.7.0)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.2.0)
|
||||
|
@ -369,18 +398,18 @@ GEM
|
|||
sqlite3 (1.3.11)
|
||||
telegramAPI (1.2.2)
|
||||
rest-client (~> 2.0, >= 1.7.3)
|
||||
term-ansicolor (1.3.2)
|
||||
term-ansicolor (1.6.0)
|
||||
tins (~> 1.0)
|
||||
test-unit (3.2.1)
|
||||
power_assert
|
||||
therubyracer (0.12.2)
|
||||
libv8 (~> 3.16.14.0)
|
||||
therubyracer (0.12.3)
|
||||
libv8 (~> 3.16.14.15)
|
||||
ref
|
||||
thor (0.19.1)
|
||||
thor (0.19.4)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.5)
|
||||
tins (1.13.0)
|
||||
twitter (5.16.0)
|
||||
tins (1.15.0)
|
||||
twitter (5.17.0)
|
||||
addressable (~> 2.3)
|
||||
buftok (~> 0.2.0)
|
||||
equalizer (= 0.0.10)
|
||||
|
@ -391,30 +420,37 @@ GEM
|
|||
memoizable (~> 0.4.0)
|
||||
naught (~> 1.0)
|
||||
simple_oauth (~> 0.3.0)
|
||||
tzinfo (1.2.2)
|
||||
tzinfo (1.2.3)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (3.0.2)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.2)
|
||||
unf_ext (0.0.7.4)
|
||||
unicode-display_width (1.1.1)
|
||||
unicorn (5.2.0)
|
||||
unicorn (5.3.0)
|
||||
kgio (~> 2.6)
|
||||
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)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
websocket (1.2.3)
|
||||
writeexcel (1.0.5)
|
||||
zendesk_api (1.14.0)
|
||||
zendesk_api (1.14.4)
|
||||
faraday (~> 0.9)
|
||||
hashie (>= 1.2, < 4.0, != 3.3.0)
|
||||
hashie (>= 3.5.2, < 4.0.0)
|
||||
inflection
|
||||
mime-types
|
||||
multipart-post (~> 2.0)
|
||||
scrub_rb (~> 1.0.1)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -423,6 +459,7 @@ DEPENDENCIES
|
|||
activerecord-nulldb-adapter
|
||||
activerecord-session_store
|
||||
argon2
|
||||
autodiscover!
|
||||
autoprefixer-rails
|
||||
biz
|
||||
browser
|
||||
|
@ -430,6 +467,7 @@ DEPENDENCIES
|
|||
coffee-rails
|
||||
coffee-script-source
|
||||
coffeelint
|
||||
composite_primary_keys
|
||||
coveralls
|
||||
daemons
|
||||
delayed_job_active_record
|
||||
|
@ -437,7 +475,6 @@ DEPENDENCIES
|
|||
doorkeeper
|
||||
eco
|
||||
em-websocket
|
||||
email_verifier
|
||||
eventmachine
|
||||
execjs
|
||||
factory_girl_rails
|
||||
|
@ -448,6 +485,7 @@ DEPENDENCIES
|
|||
guard-symlink
|
||||
htmlentities
|
||||
icalendar
|
||||
icalendar-recurrence
|
||||
json
|
||||
koala
|
||||
libv8
|
||||
|
@ -462,17 +500,19 @@ DEPENDENCIES
|
|||
omniauth-gitlab
|
||||
omniauth-google-oauth2
|
||||
omniauth-linkedin-oauth2
|
||||
omniauth-microsoft-office365
|
||||
omniauth-oauth2
|
||||
omniauth-twitter
|
||||
pg
|
||||
pre-commit
|
||||
puma
|
||||
rack-livereload
|
||||
rails (= 4.2.7.1)
|
||||
rails (= 4.2.9)
|
||||
rails-observers
|
||||
rb-fsevent
|
||||
rspec-rails
|
||||
rubocop
|
||||
rubyntlm!
|
||||
sass-rails
|
||||
selenium-webdriver
|
||||
simple-rss
|
||||
|
@ -489,12 +529,14 @@ DEPENDENCIES
|
|||
twitter
|
||||
uglifier
|
||||
unicorn
|
||||
valid_email2
|
||||
viewpoint
|
||||
webmock
|
||||
writeexcel
|
||||
zendesk_api
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.3.1p112
|
||||
ruby 2.4.1p111
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.7
|
||||
1.15.3
|
||||
|
|
|
@ -9,7 +9,7 @@ with a team of agents?
|
|||
|
||||
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)
|
||||
- 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)
|
||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1.6.x
|
||||
2.1.x
|
||||
|
|
|
@ -61,13 +61,13 @@ class App.Controller extends Spine.Controller
|
|||
clearDelay: (delay_id) =>
|
||||
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)
|
||||
|
||||
clearInterval: (interval_id) =>
|
||||
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)
|
||||
|
||||
releaseController: =>
|
||||
|
@ -185,6 +185,17 @@ class App.Controller extends Spine.Controller
|
|||
formValidate: (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) ->
|
||||
#
|
||||
|
||||
|
@ -344,7 +355,10 @@ class App.Controller extends Spine.Controller
|
|||
title: ->
|
||||
userId = $(@).data('id')
|
||||
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: ->
|
||||
userId = $(@).data('id')
|
||||
user = App.User.fullLocal(userId)
|
||||
|
|
|
@ -229,7 +229,7 @@ class App.ControllerForm extends App.Controller
|
|||
if attribute.type is 'hidden'
|
||||
attribute.autocomplete = ''
|
||||
else
|
||||
attribute.autocomplete = 'autocomplete="new-password"'
|
||||
attribute.autocomplete = 'autocomplete="off"'
|
||||
else
|
||||
attribute.autocomplete = 'autocomplete="' + attribute.autocomplete + '"'
|
||||
|
||||
|
@ -426,8 +426,11 @@ class App.ControllerForm extends App.Controller
|
|||
delete param[item.name]
|
||||
continue
|
||||
|
||||
# collect all params, push it to an array if already exists
|
||||
value = item.value.trim()
|
||||
# collect all params, push it to an array item.value already exists
|
||||
value = item.value
|
||||
if item.value
|
||||
value = item.value.trim()
|
||||
|
||||
if item.type is 'boolean'
|
||||
if value is ''
|
||||
value = undefined
|
||||
|
|
|
@ -309,6 +309,23 @@ class App.ControllerConfirm extends App.ControllerModal
|
|||
if @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
|
||||
constructor: (params) ->
|
||||
super
|
||||
|
@ -659,8 +676,9 @@ class App.Sidebar extends App.Controller
|
|||
|
||||
render: =>
|
||||
localEl = $(App.view('generic/sidebar_tabs')(
|
||||
items: @items
|
||||
items: @items
|
||||
scrollbarWidth: App.Utils.getScrollBarWidth()
|
||||
dir: App.i18n.dir()
|
||||
))
|
||||
|
||||
# init content callback
|
||||
|
|
|
@ -21,7 +21,6 @@ class App.ChannelChat extends App.ControllerSubContent
|
|||
'.js-chat-welcome': 'chatWelcome'
|
||||
'.js-testurl-input': 'urlInput'
|
||||
'.js-backgroundColor': 'chatBackground'
|
||||
'.js-paramsBlock': 'paramsBlock'
|
||||
'.js-code': 'code'
|
||||
'.js-palette': 'palette'
|
||||
'.js-color': 'colorField'
|
||||
|
@ -361,7 +360,7 @@ class App.ChannelChat extends App.ControllerSubContent
|
|||
@$('.js-modal-params').html(paramString)
|
||||
|
||||
# highlight
|
||||
@paramsBlock.each (i, block) ->
|
||||
@code.each (i, block) ->
|
||||
hljs.highlightBlock block
|
||||
|
||||
App.Config.set('Chat', { prio: 4000, name: 'Chat', parent: '#channels', target: '#channels/chat', controller: App.ChannelChat, permission: ['admin.chat'] }, 'NavBarAdmin')
|
||||
|
|
|
@ -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>' )
|
||||
|
||||
description = '''
|
||||
With Filters you can e. g. dispatch new Tickets into certain groups or set a certain priority for Tickets of an VIP customer.
|
||||
'''
|
||||
description = 'With filters you can e. g. dispatch new tickets into certain groups or set a certain priority for tickets of a VIP customer.'
|
||||
|
||||
new App.ControllerTable(
|
||||
el: template.find('.overview')
|
||||
|
@ -110,7 +108,7 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
|
|||
# show errors in form
|
||||
if errors
|
||||
@log 'error', errors
|
||||
@formValidate( form: e.target, errors: errors )
|
||||
@formValidate(form: e.target, errors: errors)
|
||||
return false
|
||||
|
||||
# disable form
|
||||
|
@ -120,8 +118,10 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
|
|||
object.save(
|
||||
done: =>
|
||||
@close()
|
||||
fail: =>
|
||||
@close()
|
||||
fail: (settings, details) =>
|
||||
@log 'errors', details
|
||||
@formEnable(e)
|
||||
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
|
||||
)
|
||||
|
||||
class App.ChannelEmailSignature extends App.Controller
|
||||
|
@ -203,7 +203,7 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
|
|||
# show errors in form
|
||||
if errors
|
||||
@log 'error', errors
|
||||
@formValidate( form: e.target, errors: errors )
|
||||
@formValidate(form: e.target, errors: errors)
|
||||
return false
|
||||
|
||||
# disable form
|
||||
|
@ -213,8 +213,10 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
|
|||
object.save(
|
||||
done: =>
|
||||
@close()
|
||||
fail: =>
|
||||
fail: (settings, details) =>
|
||||
@log 'errors', details
|
||||
@formEnable(e)
|
||||
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
|
||||
)
|
||||
|
||||
class App.ChannelEmailAccountOverview extends App.Controller
|
||||
|
@ -533,8 +535,8 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
|
||||
# base
|
||||
configureAttributesBase = [
|
||||
{ name: 'realname', display: 'Organization & Department Name', tag: 'input', type: 'text', limit: 160, null: false, placeholder: 'Organization Support', autocomplete: 'new-password' },
|
||||
{ name: 'email', display: 'Email', tag: 'input', type: 'email', limit: 120, null: false, placeholder: 'support@example.com', autocapitalize: false, 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: 'off' },
|
||||
{ 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 },
|
||||
]
|
||||
|
@ -562,21 +564,24 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
|
||||
# inbound
|
||||
configureAttributesInbound = [
|
||||
{ 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::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', },
|
||||
{ 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::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: '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::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::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::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) ->
|
||||
return if !params
|
||||
if params.adapter is 'imap'
|
||||
ui.show('options::folder')
|
||||
ui.show('options::keep_on_server')
|
||||
return
|
||||
ui.hide('options::folder')
|
||||
ui.hide('options::keep_on_server')
|
||||
|
||||
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
||||
return if !params
|
||||
|
@ -608,9 +613,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
# fill user / password based on intro info
|
||||
channel_used = { options: {} }
|
||||
if @account['meta']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
channel_used['options']['password'] = @account['meta']['password']
|
||||
channel_used['options']['folder'] = @account['meta']['folder']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
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
|
||||
@$('.base-outbound-settings').html('')
|
||||
|
@ -618,7 +624,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
if adapter is 'smtp'
|
||||
configureAttributesOutbound = [
|
||||
{ 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::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
|
||||
@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)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
||||
|
@ -726,7 +732,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
# remember account settings
|
||||
@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)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
||||
|
@ -932,7 +938,7 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
|
|||
if adapter is 'smtp'
|
||||
configureAttributesOutbound = [
|
||||
{ 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::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
||||
]
|
||||
|
|
|
@ -3,12 +3,14 @@ class App.ChannelForm extends App.ControllerSubContent
|
|||
requiredPermission: 'admin.channel_formular'
|
||||
header: 'Form'
|
||||
events:
|
||||
'change form.js-params': 'updateParams'
|
||||
'keyup form.js-params': 'updateParams'
|
||||
'change form.js-paramsDesigner': 'updateParamsDesigner'
|
||||
'keyup form.js-paramsDesigner': 'updateParamsDesigner'
|
||||
'change .js-formSetting input': 'toggleFormSetting'
|
||||
'change .js-paramsSetting select': 'updateGroup'
|
||||
|
||||
elements:
|
||||
'.js-paramsBlock': 'paramsBlock'
|
||||
'.js-code': 'code'
|
||||
'.js-paramsSetting': 'paramsSetting'
|
||||
'.js-formSetting input': 'formSetting'
|
||||
|
||||
constructor: ->
|
||||
|
@ -20,22 +22,38 @@ class App.ChannelForm extends App.ControllerSubContent
|
|||
|
||||
render: =>
|
||||
setting = App.Setting.get('form_ticket_create')
|
||||
@html App.view('channel/form')(
|
||||
|
||||
element = $(App.view('channel/form')(
|
||||
baseurl: window.location.origin
|
||||
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
|
||||
|
||||
@updateParams()
|
||||
@updateParamsDesigner()
|
||||
|
||||
updateParams: ->
|
||||
updateParamsDesigner: ->
|
||||
quote = (string) ->
|
||||
string = string.replace('\'', '\\\'')
|
||||
.replace(/\</g, '<')
|
||||
.replace(/\>/g, '>')
|
||||
params = @formParam(@$('.js-params'))
|
||||
params = @formParam(@$('.js-paramsDesigner'))
|
||||
paramString = ''
|
||||
for key, value of params
|
||||
if value != ''
|
||||
|
@ -63,4 +81,8 @@ class App.ChannelForm extends App.ControllerSubContent
|
|||
value = @formSetting.prop('checked')
|
||||
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')
|
||||
|
|
|
@ -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'
|
||||
)
|
|
@ -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'
|
||||
)
|
|
@ -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'
|
||||
)
|
|
@ -28,13 +28,20 @@ class Index extends App.ControllerIntegrationBase
|
|||
super
|
||||
active = @$('.js-switch input').prop('checked')
|
||||
if active
|
||||
@ajax(
|
||||
id: 'jobs_config'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/integration/ldap/job_start"
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@render(true)
|
||||
job_start = =>
|
||||
@ajax(
|
||||
id: 'jobs_config'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/integration/ldap/job_start"
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@render(true)
|
||||
)
|
||||
|
||||
App.Delay.set(
|
||||
job_start,
|
||||
600,
|
||||
'job_start',
|
||||
)
|
||||
|
||||
class Form extends App.Controller
|
||||
|
@ -61,8 +68,15 @@ class Form extends App.Controller
|
|||
render: (top = false) =>
|
||||
@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')(
|
||||
config: @config
|
||||
config: @config,
|
||||
group_role_map: group_role_map
|
||||
)
|
||||
if _.isEmpty(@config)
|
||||
@$('.js-notConfigured').removeClass('hide')
|
||||
|
@ -84,6 +98,7 @@ class Form extends App.Controller
|
|||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@render(true)
|
||||
@lastResult()
|
||||
)
|
||||
|
||||
startWizard: (e) =>
|
||||
|
@ -280,12 +295,13 @@ class ConnectionWizard extends App.WizardModal
|
|||
|
||||
option = ''
|
||||
options = {}
|
||||
for dn in data.attributes.namingcontexts
|
||||
options[dn] = dn
|
||||
if option is ''
|
||||
option = dn
|
||||
if option.length > dn.length
|
||||
option = dn
|
||||
if !_.isEmpty data.attributes
|
||||
for dn in data.attributes.namingcontexts
|
||||
options[dn] = dn
|
||||
if option is ''
|
||||
option = dn
|
||||
if option.length > dn.length
|
||||
option = dn
|
||||
|
||||
@wizardConfig.options = options
|
||||
@wizardConfig.option = option
|
||||
|
@ -419,7 +435,9 @@ class ConnectionWizard extends App.WizardModal
|
|||
length = group_role_map.source.length-1
|
||||
for count in [0..length]
|
||||
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
|
||||
|
||||
expertSettings = @formParam(@expertForm)
|
||||
|
@ -454,8 +472,9 @@ class ConnectionWizard extends App.WizardModal
|
|||
|
||||
buildRowsGroupRole: (group_role_map) =>
|
||||
el = []
|
||||
for source, dest of group_role_map
|
||||
el.push @buildRowGroupRole(source, dest)
|
||||
for source, dests of group_role_map
|
||||
for dest in dests
|
||||
el.push @buildRowGroupRole(source, dest)
|
||||
el
|
||||
|
||||
buildRowGroupRole: (source, dest) =>
|
||||
|
|
|
@ -9,43 +9,7 @@ class Index extends App.ControllerSubContent
|
|||
@render()
|
||||
|
||||
render: =>
|
||||
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_provider_all = App.Config.get('auth_provider_all')
|
||||
auth_providers = {}
|
||||
for key, provider of auth_provider_all
|
||||
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('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'
|
||||
})
|
||||
|
|
|
@ -75,13 +75,14 @@ class Index extends App.ControllerSubContent
|
|||
groups = []
|
||||
group_ids = @Session.get('group_ids')
|
||||
if group_ids
|
||||
for group_id in group_ids
|
||||
group = App.Group.find(group_id)
|
||||
groups.push group
|
||||
if !user_group_config
|
||||
if !config['group_ids']
|
||||
config['group_ids'] = []
|
||||
config['group_ids'].push group_id.toString()
|
||||
for group_id, access of group_ids
|
||||
if _.contains(access, 'full')
|
||||
group = App.Group.find(group_id)
|
||||
groups.push group
|
||||
if !user_group_config
|
||||
if !config['group_ids']
|
||||
config['group_ids'] = []
|
||||
config['group_ids'].push group_id.toString()
|
||||
|
||||
for sound in @sounds
|
||||
sound.selected = sound.file is App.OnlineNotification.soundFile() ? true : false
|
||||
|
@ -90,7 +91,7 @@ class Index extends App.ControllerSubContent
|
|||
groups: groups
|
||||
config: config
|
||||
sounds: @sounds
|
||||
notification_sound_enabled: App.OnlineNotification.soundEnabled()
|
||||
notificationSoundEnabled: App.OnlineNotification.soundEnabled()
|
||||
|
||||
update: (e) =>
|
||||
|
||||
|
|
|
@ -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')
|
|
@ -2,7 +2,7 @@ class App.SettingsAreaProxy extends App.Controller
|
|||
events:
|
||||
'submit form': 'update'
|
||||
'click .js-submit': 'update'
|
||||
'click .js-test': 'test2'
|
||||
'click .js-test': 'testConnection'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
@ -14,20 +14,21 @@ class App.SettingsAreaProxy extends App.Controller
|
|||
proxy: App.Setting.get('proxy')
|
||||
proxy_username: App.Setting.get('proxy_username')
|
||||
proxy_password: App.Setting.get('proxy_password')
|
||||
proxy_no: App.Setting.get('proxy_no')
|
||||
)
|
||||
|
||||
update: (e) =>
|
||||
e.preventDefault()
|
||||
@formDisable(e)
|
||||
params = @formParam(e)
|
||||
console.log('params', params)
|
||||
App.Setting.set('proxy', params.proxy)
|
||||
App.Setting.set('proxy_username', params.proxy_username)
|
||||
App.Setting.set('proxy_password', params.proxy_password)
|
||||
App.Setting.set('proxy_no', params.proxy_no)
|
||||
@formEnable(e)
|
||||
@render()
|
||||
|
||||
test2: (e) =>
|
||||
testConnection: (e) =>
|
||||
e.preventDefault()
|
||||
params = @formParam(e)
|
||||
@ajax(
|
||||
|
|
|
@ -15,7 +15,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
attribute: attribute
|
||||
params: params
|
||||
))
|
||||
@[localParams.data_type](element, localParams, params)
|
||||
@[localParams.data_type](element, localParams, params, attribute)
|
||||
localItem.find('.js-dataMap').html(element)
|
||||
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'
|
||||
input: 'Text'
|
||||
select: 'Select'
|
||||
tree_select: 'Tree Select'
|
||||
boolean: 'Boolean'
|
||||
integer: 'Integer'
|
||||
|
||||
|
@ -81,9 +82,9 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
view:
|
||||
shown: true
|
||||
invite_customer:
|
||||
show: false
|
||||
shown: false
|
||||
required: false
|
||||
'admin.group':
|
||||
'admin.user':
|
||||
create:
|
||||
shown: true
|
||||
required: false
|
||||
|
@ -93,10 +94,10 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
view:
|
||||
shown: true
|
||||
invite_agent:
|
||||
show: false
|
||||
shown: false
|
||||
required: false
|
||||
invite_customer:
|
||||
show: false
|
||||
shown: false
|
||||
required: false
|
||||
Organization:
|
||||
'ticket.customer':
|
||||
|
@ -111,7 +112,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
required: false
|
||||
view:
|
||||
shown: true
|
||||
'admin.group':
|
||||
'admin.organization':
|
||||
create:
|
||||
shown: true
|
||||
required: false
|
||||
|
@ -308,6 +309,69 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
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) ->
|
||||
lastSelected = undefined
|
||||
item.on('click', '.js-selected', (e) ->
|
||||
|
|
|
@ -4,10 +4,25 @@ class App.UiElement.permission extends App.UiElement.ApplicationUiElement
|
|||
|
||||
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')(
|
||||
attribute: attribute
|
||||
params: params
|
||||
permissions: permissions
|
||||
groups: groups
|
||||
groupsSelected: groupsSelected
|
||||
groupAccesses: App.Group.accesses()
|
||||
) )
|
||||
|
||||
# show/hide trees
|
||||
|
|
|
@ -20,7 +20,7 @@ class App.UiElement.postmaster_set
|
|||
name: 'Customer'
|
||||
relation: 'User'
|
||||
tag: 'user_autocompletion'
|
||||
disableCreateUser: true
|
||||
disableCreateObject: true
|
||||
}
|
||||
{
|
||||
value: 'group_id'
|
||||
|
@ -32,7 +32,7 @@ class App.UiElement.postmaster_set
|
|||
name: 'Owner'
|
||||
relation: 'User'
|
||||
tag: 'user_autocompletion'
|
||||
disableCreateUser: true
|
||||
disableCreateObject: true
|
||||
}
|
||||
]
|
||||
article:
|
||||
|
|
|
@ -9,24 +9,24 @@ class App.UiElement.searchable_select extends App.UiElement.ApplicationUiElement
|
|||
attribute.multiple = ''
|
||||
|
||||
# build options list based on config
|
||||
@getConfigOptionList( attribute, params )
|
||||
@getConfigOptionList(attribute, params)
|
||||
|
||||
# build options list based on relation
|
||||
@getRelationOptionList( attribute, params )
|
||||
@getRelationOptionList(attribute, params)
|
||||
|
||||
# add null selection if needed
|
||||
@addNullOption( attribute, params )
|
||||
@addNullOption(attribute, params)
|
||||
|
||||
# sort attribute.options
|
||||
@sortOptions( attribute, params )
|
||||
@sortOptions(attribute, params)
|
||||
|
||||
# finde selected/checked item of list
|
||||
@selectedOptions( attribute, params )
|
||||
@selectedOptions(attribute, params)
|
||||
|
||||
# disable item of list
|
||||
@disabledOptions( attribute, params )
|
||||
@disabledOptions(attribute, params)
|
||||
|
||||
# filter attributes
|
||||
@filterOption( attribute, params )
|
||||
@filterOption(attribute, params)
|
||||
|
||||
new App.SearchableSelect( attribute: attribute ).element()
|
||||
new App.SearchableSelect(attribute: attribute).element()
|
||||
|
|
|
@ -33,14 +33,15 @@ class App.UiElement.ticket_perform_action
|
|||
elements["#{groupKey}.#{config.name}"] = config
|
||||
|
||||
# add ticket deletion action
|
||||
elements['ticket.action'] =
|
||||
name: 'action'
|
||||
display: 'Action'
|
||||
tag: 'select'
|
||||
null: false
|
||||
translate: true
|
||||
options:
|
||||
delete: 'delete'
|
||||
if attribute.ticket_delete
|
||||
elements['ticket.action'] =
|
||||
name: 'action'
|
||||
display: 'Action'
|
||||
tag: 'select'
|
||||
null: false
|
||||
translate: true
|
||||
options:
|
||||
delete: 'Delete'
|
||||
|
||||
[defaults, groups, elements]
|
||||
|
||||
|
|
|
@ -156,7 +156,7 @@ class App.UiElement.ticket_selector
|
|||
elementRow = $(e.target).closest('.js-filterElement')
|
||||
groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
|
||||
return if !groupAndAttribute
|
||||
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute, false)
|
||||
@buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
|
||||
)
|
||||
|
||||
# bind for preview
|
||||
|
@ -244,9 +244,9 @@ class App.UiElement.ticket_selector
|
|||
if 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')
|
||||
|
||||
name = "#{attribute.name}::#{groupAndAttribute}::operator"
|
||||
|
@ -284,9 +284,9 @@ class App.UiElement.ticket_selector
|
|||
|
||||
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')
|
||||
currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
|
||||
|
||||
|
@ -318,7 +318,6 @@ class App.UiElement.ticket_selector
|
|||
if !preCondition
|
||||
elementRow.find('.js-preCondition select').html('')
|
||||
elementRow.find('.js-preCondition').addClass('hide')
|
||||
return if !buildValue
|
||||
toggleValue()
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
return
|
||||
|
@ -351,7 +350,6 @@ class App.UiElement.ticket_selector
|
|||
toggleValue()
|
||||
)
|
||||
|
||||
return if !buildValue
|
||||
@buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||
toggleValue()
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -2,5 +2,5 @@
|
|||
class App.UiElement.user_autocompletion_search
|
||||
@render: (attributeOrig, params = {}) ->
|
||||
attribute = _.clone(attributeOrig)
|
||||
attribute.disableCreateUser = true
|
||||
attribute.disableCreateObject = true
|
||||
new App.UserOrganizationAutocompletion(attribute: attribute, params: params).element()
|
||||
|
|
|
@ -72,6 +72,7 @@ class App.UiElement.user_permission
|
|||
rolesSelected: rolesSelected
|
||||
groupsSelected: groupsSelected
|
||||
hideGroups: hideGroups
|
||||
groupAccesses: App.Group.accesses()
|
||||
) )
|
||||
|
||||
# if customer, remove admin and agent
|
||||
|
@ -105,7 +106,7 @@ class App.UiElement.user_permission
|
|||
|
||||
# select groups if only one is available
|
||||
if hideGroups
|
||||
item.find('.js-groupList [name=group_ids]').prop('checked', false)
|
||||
item.find('.js-groupList .js-groupListItem[value=full]').prop('checked', false)
|
||||
return
|
||||
|
||||
# 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
|
||||
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
|
||||
trigger.trigger('change')
|
||||
|
|
|
@ -9,6 +9,7 @@ class App.TicketCreate extends App.Controller
|
|||
|
||||
constructor: (params) ->
|
||||
super
|
||||
@sidebarState = {}
|
||||
|
||||
# define default type
|
||||
@default_type = 'phone-in'
|
||||
|
@ -91,6 +92,8 @@ class App.TicketCreate extends App.Controller
|
|||
else
|
||||
@$('[name="cc"]').closest('.form-group').addClass('hide')
|
||||
|
||||
App.TaskManager.touch(@task_key)
|
||||
|
||||
meta: =>
|
||||
text = ''
|
||||
if @articleAttributes
|
||||
|
@ -99,10 +102,10 @@ class App.TicketCreate extends App.Controller
|
|||
if title
|
||||
text = "#{text}: #{title}"
|
||||
meta =
|
||||
url: @url()
|
||||
head: text
|
||||
title: text
|
||||
id: @id
|
||||
url: @url()
|
||||
head: text
|
||||
title: text
|
||||
id: @id
|
||||
iconClass: 'pen'
|
||||
|
||||
url: =>
|
||||
|
@ -228,7 +231,7 @@ class App.TicketCreate extends App.Controller
|
|||
type = @$('[name="formSenderType"]').val()
|
||||
|
||||
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]')
|
||||
if App.Utils.signatureCheck(body.html() || '', signatureFinished)
|
||||
|
@ -330,27 +333,47 @@ class App.TicketCreate extends App.Controller
|
|||
# show text module UI
|
||||
@textModule = new App.WidgetTextModule(
|
||||
el: @$('[data-name="body"]').parent()
|
||||
)
|
||||
|
||||
new Sidebar(
|
||||
el: @sidebar
|
||||
params: @formDefault
|
||||
textModule: @textModule
|
||||
data:
|
||||
config: App.Config.all()
|
||||
user: App.Session.get()
|
||||
)
|
||||
|
||||
$('#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
|
||||
App.TaskManager.touch(@task_key)
|
||||
|
||||
localUserInfo: (e) =>
|
||||
|
||||
return if !@sidebarWidget
|
||||
params = App.ControllerForm.params($(e.target).closest('form'))
|
||||
|
||||
new Sidebar(
|
||||
el: @sidebar
|
||||
params: params
|
||||
textModule: @textModule
|
||||
if params.customer_id
|
||||
callback = (customer) =>
|
||||
@localUserInfoCallback(params, customer)
|
||||
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) ->
|
||||
|
@ -475,11 +498,16 @@ class App.TicketCreate extends App.Controller
|
|||
# scroll to top
|
||||
ui.scrollTo()
|
||||
|
||||
# add sidebar params
|
||||
if ui.sidebarWidget
|
||||
ui.sidebarWidget.commit(ticket_id: @id)
|
||||
|
||||
# access to group
|
||||
group_ids = _.map(App.Session.get('group_ids'), (id) -> id.toString())
|
||||
if group_ids && _.contains(group_ids, @group_id.toString())
|
||||
ui.navigate "#ticket/zoom/#{@id}"
|
||||
return
|
||||
for group_id, access of App.Session.get('group_ids')
|
||||
if @group_id.toString() is group_id.toString()
|
||||
if _.contains(access, 'read') || _.contains(access, 'full')
|
||||
ui.navigate "#ticket/zoom/#{@id}"
|
||||
return
|
||||
|
||||
# if not, show start screen
|
||||
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
|
||||
requiredPermission: 'ticket.agent'
|
||||
constructor: (params) ->
|
||||
|
@ -617,6 +537,9 @@ class Router extends App.ControllerPermanent
|
|||
if params.customer_id
|
||||
split = "/customer/#{params.customer_id}"
|
||||
|
||||
if params.query
|
||||
split = "/query/#{params.query}"
|
||||
|
||||
id = Math.floor( Math.random() * 99999 )
|
||||
@navigate "#ticket/create/id/#{id}#{split}"
|
||||
return
|
||||
|
@ -627,6 +550,7 @@ class Router extends App.ControllerPermanent
|
|||
article_id: params.article_id
|
||||
type: params.type
|
||||
customer_id: params.customer_id
|
||||
query: params.query
|
||||
id: params.id
|
||||
|
||||
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/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
|
||||
App.Config.set('ticket/create/:ticket_id/:article_id', Router, 'Routes')
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -98,9 +98,13 @@ class App.TicketMerge extends App.ControllerModal
|
|||
type: 'error'
|
||||
msg: App.i18n.translateContent(data['message'])
|
||||
timeout: 6000
|
||||
|
||||
@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)
|
||||
)
|
||||
|
|
|
@ -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: =>
|
||||
@input.focus()
|
||||
|
||||
|
@ -473,7 +481,7 @@ class ChatWindow extends App.Controller
|
|||
if event.data and event.data.callback
|
||||
event.data.callback()
|
||||
|
||||
@$('.js-customerChatInput').ce({
|
||||
@input.ce({
|
||||
mode: 'richtext'
|
||||
multiline: true
|
||||
maxlength: 40000
|
||||
|
@ -522,7 +530,7 @@ class ChatWindow extends App.Controller
|
|||
|
||||
switch event.keyCode
|
||||
when TABKEY
|
||||
allChatInputs = $('.js-customerChatInput').not('[disabled="disabled"]')
|
||||
allChatInputs = @input.not('[disabled="disabled"]')
|
||||
chatCount = allChatInputs.size()
|
||||
index = allChatInputs.index(@input)
|
||||
|
||||
|
@ -542,7 +550,7 @@ class ChatWindow extends App.Controller
|
|||
allChatInputs.eq(chatCount-1).focus()
|
||||
|
||||
when ENTERKEY
|
||||
if !event.shiftKey
|
||||
if !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey
|
||||
event.preventDefault()
|
||||
@sendMessage()
|
||||
|
||||
|
@ -587,7 +595,7 @@ class ChatWindow extends App.Controller
|
|||
@sounds.message.play()
|
||||
@notifyDesktop(
|
||||
title: @name
|
||||
body: message
|
||||
body: App.Utils.html2text(message)
|
||||
url: '#customer_chat'
|
||||
callback: =>
|
||||
App.Event.trigger('chat_focus', { session_id: @session.session_id })
|
||||
|
|
|
@ -450,8 +450,8 @@ class EmailNotification extends App.WizardFullScreen
|
|||
if adapter is 'smtp'
|
||||
configureAttributesOutbound = [
|
||||
{ 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::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||
{ 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: 'off', single: true },
|
||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
||||
]
|
||||
@form = new App.ControllerForm(
|
||||
|
@ -671,20 +671,24 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
|
||||
# inbound
|
||||
configureAttributesInbound = [
|
||||
{ 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::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', },
|
||||
{ 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::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
||||
{ 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::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: '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::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) ->
|
||||
return if !params
|
||||
if params.adapter is 'imap'
|
||||
ui.show('options::folder')
|
||||
ui.show('options::keep_on_server')
|
||||
return
|
||||
ui.hide('options::folder')
|
||||
ui.hide('options::keep_on_server')
|
||||
|
||||
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
||||
return if !params
|
||||
|
@ -700,7 +704,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
return
|
||||
|
||||
new App.ControllerForm(
|
||||
el: @$('.base-inbound-settings'),
|
||||
el: @$('.base-inbound-settings')
|
||||
model:
|
||||
configure_attributes: configureAttributesInbound
|
||||
className: ''
|
||||
|
@ -716,8 +720,10 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
# fill user / password based on intro info
|
||||
channel_used = { options: {} }
|
||||
if @account['meta']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
channel_used['options']['password'] = @account['meta']['password']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
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
|
||||
@$('.base-outbound-settings').html('')
|
||||
|
@ -725,8 +731,8 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
if adapter is 'smtp'
|
||||
configureAttributesOutbound = [
|
||||
{ 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::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||
{ 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: 'off', single: true },
|
||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
||||
]
|
||||
@form = new App.ControllerForm(
|
||||
|
@ -745,7 +751,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
@account.meta = params
|
||||
|
||||
@disable(e)
|
||||
@$('.js-probe .js-email').text( params.email )
|
||||
@$('.js-probe .js-email').text(params.email)
|
||||
@showSlide('js-probe')
|
||||
|
||||
@ajax(
|
||||
|
@ -760,7 +766,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
for key, value of data.setting
|
||||
@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)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
||||
|
@ -809,7 +815,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
# remember account settings
|
||||
@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)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
||||
|
|
|
@ -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, @)
|
||||
|
|
@ -10,6 +10,7 @@ class Index extends App.ControllerContent
|
|||
'.zendesk-api-token-error': 'apiTokenErrorMessage'
|
||||
'#zendesk-email': 'zendeskEmail'
|
||||
'#zendesk-api-token': 'zendeskApiToken'
|
||||
'.js-ticket-count-info': 'ticketCountInfo'
|
||||
updateMigrationDisplayLoop: 0
|
||||
|
||||
events:
|
||||
|
@ -116,7 +117,8 @@ class Index extends App.ControllerContent
|
|||
showCredentials: (e) =>
|
||||
e.preventDefault()
|
||||
@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')
|
||||
@$('[data-slide=zendesk-url]').toggleClass('hide')
|
||||
@$('[data-slide=zendesk-credentials]').toggleClass('hide')
|
||||
|
@ -171,6 +173,10 @@ class Index extends App.ControllerContent
|
|||
for key, item of data.data
|
||||
if item.done > item.total
|
||||
item.done = item.total
|
||||
|
||||
if key == 'Ticket' && item.total >= 1000
|
||||
@ticketCountInfo.removeClass('hide')
|
||||
|
||||
element = @$('.js-' + key.toLowerCase() )
|
||||
element.find('.js-done').text(item.done)
|
||||
element.find('.js-total').text(item.total)
|
||||
|
|
|
@ -1499,7 +1499,7 @@ class InputsRef extends App.ControllerContent
|
|||
null: false
|
||||
relation: 'User'
|
||||
autocapitalize: false
|
||||
disableCreateUser: true
|
||||
disableCreateObject: true
|
||||
multiple: true
|
||||
|
||||
@$('.userOrganizationAutocompletePlaceholder').replaceWith( userOrganizationAutocomplete.element() )
|
||||
|
|
|
@ -38,50 +38,7 @@ class Index extends App.ControllerContent
|
|||
)
|
||||
|
||||
render: (data = {}) ->
|
||||
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_provider_all = App.Config.get('auth_provider_all')
|
||||
auth_providers = []
|
||||
for key, provider of auth_provider_all
|
||||
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
|
||||
|
|
|
@ -1,4 +1,46 @@
|
|||
# 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
|
||||
requiredPermission: 'admin.object'
|
||||
constructor: ->
|
||||
|
@ -135,6 +177,7 @@ class New extends App.ControllerGenericNew
|
|||
|
||||
onSubmit: (e) =>
|
||||
params = @formParam(e.target)
|
||||
params = treeParams(e, params)
|
||||
|
||||
# show attributes for create_middle in two column style
|
||||
if params.screens && params.screens.create_middle
|
||||
|
@ -184,6 +227,8 @@ class Edit extends App.ControllerGenericEdit
|
|||
#if attribute.name is 'data_type'
|
||||
# attribute.disabled = true
|
||||
|
||||
console.log('configure_attributes', configure_attributes)
|
||||
|
||||
@controller = new App.ControllerForm(
|
||||
model:
|
||||
configure_attributes: configure_attributes
|
||||
|
@ -195,6 +240,7 @@ class Edit extends App.ControllerGenericEdit
|
|||
|
||||
onSubmit: (e) =>
|
||||
params = @formParam(e.target)
|
||||
params = treeParams(e, params)
|
||||
|
||||
# show attributes for create_middle in two column style
|
||||
if params.screens && params.screens.create_middle
|
||||
|
|
|
@ -6,7 +6,7 @@ class App.TicketCustomer extends App.ControllerModal
|
|||
|
||||
content: ->
|
||||
configure_attributes = [
|
||||
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, 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(
|
||||
model:
|
||||
|
|
|
@ -221,7 +221,7 @@ class App.TicketOverview extends App.Controller
|
|||
if @batchCountIndex == @batchCount
|
||||
App.Event.trigger('overview:fetch')
|
||||
)
|
||||
return
|
||||
return
|
||||
|
||||
if action is 'group_assign'
|
||||
@batchCount = items.length
|
||||
|
|
|
@ -401,10 +401,11 @@ class App.TicketZoom extends App.Controller
|
|||
nav: @nav
|
||||
isCustomer: @permissionCheck('ticket.customer')
|
||||
scrollbarWidth: App.Utils.getScrollBarWidth()
|
||||
dir: App.i18n.dir()
|
||||
)
|
||||
|
||||
new App.TicketZoomOverviewNavigator(
|
||||
el: elLocal.find('.overview-navigator')
|
||||
el: elLocal.find('.js-overviewNavigatorContainer')
|
||||
ticket_id: @ticket_id
|
||||
overview_id: @overview_id
|
||||
)
|
||||
|
@ -412,13 +413,13 @@ class App.TicketZoom extends App.Controller
|
|||
new App.TicketZoomTitle(
|
||||
object_id: @ticket_id
|
||||
overview_id: @overview_id
|
||||
el: elLocal.find('.ticket-title')
|
||||
el: elLocal.find('.js-ticketTitleContainer')
|
||||
task_key: @task_key
|
||||
)
|
||||
|
||||
new App.TicketZoomMeta(
|
||||
object_id: @ticket_id
|
||||
el: elLocal.find('.ticket-meta')
|
||||
el: elLocal.find('.js-ticketMetaContainer')
|
||||
)
|
||||
|
||||
@attributeBar = new App.TicketZoomAttributeBar(
|
||||
|
@ -445,7 +446,12 @@ class App.TicketZoom extends App.Controller
|
|||
)
|
||||
|
||||
@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
|
||||
)
|
||||
|
||||
|
@ -467,6 +473,7 @@ class App.TicketZoom extends App.Controller
|
|||
sidebarState: @sidebarState
|
||||
object_id: @ticket_id
|
||||
model: 'Ticket'
|
||||
query: @query
|
||||
taskGet: @taskGet
|
||||
task_key: @task_key
|
||||
formMeta: @formMeta
|
||||
|
@ -557,14 +564,16 @@ class App.TicketZoom extends App.Controller
|
|||
return if !@ticket
|
||||
currentStoreTicket = @ticket.attributes()
|
||||
delete currentStoreTicket.article
|
||||
internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
|
||||
currentStore =
|
||||
ticket: currentStoreTicket
|
||||
article:
|
||||
to: ''
|
||||
cc: ''
|
||||
subject: ''
|
||||
type: 'note'
|
||||
body: ''
|
||||
internal: 'true'
|
||||
internal: internal
|
||||
in_reply_to: ''
|
||||
|
||||
if @permissionCheck('ticket.customer')
|
||||
|
@ -575,7 +584,7 @@ class App.TicketZoom extends App.Controller
|
|||
formCurrent: =>
|
||||
currentParams =
|
||||
ticket: @formParam(@el.find('.edit'))
|
||||
article: @formParam(@el.find('.article-add'))
|
||||
article: @articleNew.params()
|
||||
|
||||
# add attachments if exist
|
||||
attachmentCount = @$('.article-add .textBubble .attachments .attachment').length
|
||||
|
@ -684,7 +693,7 @@ class App.TicketZoom extends App.Controller
|
|||
tagAdd: (tag) =>
|
||||
return if !@sidebar
|
||||
return if !@sidebar.reload
|
||||
@sidebar.reload(tagAdd: tag)
|
||||
@sidebar.reload(tagAdd: tag, source: 'macro')
|
||||
tagRemove: (tag) =>
|
||||
return if !@sidebar
|
||||
return if !@sidebar.reload
|
||||
|
@ -789,6 +798,9 @@ class App.TicketZoom extends App.Controller
|
|||
# reset form after save
|
||||
@reset()
|
||||
|
||||
if @sidebar
|
||||
@sidebar.commit()
|
||||
|
||||
if taskAction is 'closeNextInOverview'
|
||||
if @overview_id
|
||||
current_position = 0
|
||||
|
|
|
@ -391,25 +391,41 @@ class App.TicketZoomArticleActions extends App.Controller
|
|||
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
|
||||
|
||||
# check if quote need to be added
|
||||
selectedText = App.ClipBoard.getSelected()
|
||||
if selectedText
|
||||
signaturePosition = 'bottom'
|
||||
selected = App.ClipBoard.getSelected('html')
|
||||
if selected
|
||||
selected = App.Utils.htmlCleanup(selected).html()
|
||||
if !selected
|
||||
selected = App.ClipBoard.getSelected('text')
|
||||
if selected
|
||||
selected = App.Utils.textCleanup(selected)
|
||||
selected = App.Utils.text2html(selected)
|
||||
|
||||
# clean selection
|
||||
selectedText = App.Utils.textCleanup(selectedText)
|
||||
# full quote, if needed
|
||||
if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote')
|
||||
signaturePosition = 'top'
|
||||
if article.content_type.match('html')
|
||||
selected = App.Utils.textCleanup(article.body)
|
||||
if article.content_type.match('plain')
|
||||
selected = App.Utils.textCleanup(article.body)
|
||||
selected = App.Utils.text2html(selected)
|
||||
|
||||
# convert to html
|
||||
selectedText = App.Utils.text2html(selectedText)
|
||||
if selectedText
|
||||
selectedText = "<div><br><br/></div><div><blockquote type=\"cite\">#{selectedText}</blockquote></div><div><br></div>"
|
||||
if selected
|
||||
selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>"
|
||||
|
||||
# add selected text to body
|
||||
body = selectedText + body
|
||||
# add selected text to body
|
||||
body = selected + body
|
||||
|
||||
articleNew.body = body
|
||||
|
||||
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) =>
|
||||
e.preventDefault()
|
||||
|
|
|
@ -28,7 +28,62 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
constructor: ->
|
||||
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 =
|
||||
note: true
|
||||
phone: true
|
||||
|
@ -50,12 +105,9 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
possibleArticleType['email'] = true
|
||||
|
||||
# gets referenced in @setArticleType
|
||||
@internalSelector = true
|
||||
@type = @defaults['type'] || 'note'
|
||||
@articleTypes = []
|
||||
if possibleArticleType.note
|
||||
internal = @Config.get('ui_ticket_zoom_article_new_internal')
|
||||
|
||||
internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
|
||||
@articleTypes.push {
|
||||
name: 'note'
|
||||
icon: 'note'
|
||||
|
@ -64,10 +116,13 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
features: ['attachment']
|
||||
}
|
||||
if possibleArticleType.email
|
||||
attributes = ['to', 'cc', 'subject']
|
||||
if !@Config.get('ui_ticket_zoom_article_email_subject')
|
||||
attributes = ['to', 'cc']
|
||||
@articleTypes.push {
|
||||
name: 'email'
|
||||
icon: 'email'
|
||||
attributes: ['to', 'cc']
|
||||
attributes: attributes
|
||||
internal: false,
|
||||
features: ['attachment']
|
||||
}
|
||||
|
@ -80,22 +135,28 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
features: []
|
||||
}
|
||||
if possibleArticleType['twitter status']
|
||||
attributes = ['body:limit', 'body:initials']
|
||||
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
|
||||
attributes = ['body:limit']
|
||||
@articleTypes.push {
|
||||
name: 'twitter status'
|
||||
icon: 'twitter'
|
||||
attributes: []
|
||||
internal: false,
|
||||
features: ['body:limit']
|
||||
features: ['body:limit', 'body:initials']
|
||||
maxTextLength: 140
|
||||
warningTextLength: 30
|
||||
}
|
||||
if possibleArticleType['twitter direct-message']
|
||||
attributes = ['body:limit', 'body:initials']
|
||||
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
|
||||
attributes = ['body:limit']
|
||||
@articleTypes.push {
|
||||
name: 'twitter direct-message'
|
||||
icon: 'twitter'
|
||||
attributes: ['to']
|
||||
internal: false,
|
||||
features: ['body:limit']
|
||||
features: ['body:limit', 'body:initials']
|
||||
maxTextLength: 10000
|
||||
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) ->
|
||||
el.focus()
|
||||
if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined'
|
||||
|
@ -229,7 +239,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
)
|
||||
|
||||
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(
|
||||
|
@ -300,6 +310,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
data:
|
||||
ticket: ticket
|
||||
user: App.Session.get()
|
||||
config: App.Config.all()
|
||||
)
|
||||
callback = (ticket) ->
|
||||
textModule.reload(
|
||||
|
@ -318,9 +329,6 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
params.form_id = @form_id
|
||||
params.content_type = 'text/html'
|
||||
|
||||
if !params['internal']
|
||||
params['internal'] = false
|
||||
|
||||
if @permissionCheck('ticket.customer')
|
||||
sender = App.TicketArticleSender.findByAttribute('name', 'Customer')
|
||||
type = App.TicketArticleType.findByAttribute('name', 'web')
|
||||
|
@ -332,15 +340,20 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
params.sender_id = sender.id
|
||||
params.type_id = type.id
|
||||
|
||||
if params.internal
|
||||
params.internal = true
|
||||
else
|
||||
params.internal = false
|
||||
|
||||
if params.type is 'twitter status'
|
||||
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
|
||||
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'
|
||||
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
|
||||
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'
|
||||
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
|
||||
|
@ -352,6 +365,16 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
params.content_type = 'text/plain'
|
||||
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
|
||||
|
||||
validate: =>
|
||||
|
@ -411,11 +434,11 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
return false
|
||||
|
||||
if params.type is 'twitter status'
|
||||
textLength = @maxTextLength - params.body.length
|
||||
textLength = @maxTextLength - App.Utils.textLengthWithUrl(params.body)
|
||||
return false if textLength < 0
|
||||
|
||||
if params.type is 'twitter direct-message'
|
||||
textLength = @maxTextLength - params.body.length
|
||||
textLength = @maxTextLength - App.Utils.textLengthWithUrl(params.body)
|
||||
return false if textLength < 0
|
||||
|
||||
true
|
||||
|
@ -461,13 +484,15 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
|
||||
@$('[name=internal]').val('')
|
||||
|
||||
setArticleType: (type) =>
|
||||
setArticleType: (type, signaturePosition = 'bottom') =>
|
||||
wasScrolledToBottom = @isScrolledToBottom()
|
||||
@type = type
|
||||
@$('[name=type]').val(type).trigger('change')
|
||||
@articleNewEdit.attr('data-type', type)
|
||||
@$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide')
|
||||
|
||||
@setPossibleArticleTypes()
|
||||
|
||||
# get config
|
||||
config = {}
|
||||
for articleTypeConfig in @articleTypes
|
||||
|
@ -500,7 +525,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@$('[data-name=body] [data-signature="true"]').remove()
|
||||
|
||||
# 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]')
|
||||
if App.Utils.signatureCheck(body.html() || '', signatureFinished)
|
||||
|
@ -508,7 +533,10 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
body.append('<br><br>')
|
||||
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
|
||||
App.Utils.htmlStrip(signature)
|
||||
body.append(signature)
|
||||
if signaturePosition is 'top'
|
||||
body.prepend(signature)
|
||||
else
|
||||
body.append(signature)
|
||||
@$('[data-name=body]').replaceWith(body)
|
||||
|
||||
# remove old signature
|
||||
|
@ -534,13 +562,28 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
for name in articleType.features
|
||||
if name is 'attachment'
|
||||
@$('.article-attachment, .attachments').removeClass('hide')
|
||||
if name is 'body:initials'
|
||||
@updateInitials()
|
||||
if name is 'body:limit'
|
||||
@maxTextLength = articleType.maxTextLength
|
||||
@warningTextLength = articleType.warningTextLength
|
||||
@delay(@updateLetterCount, 600)
|
||||
@updateInitials()
|
||||
@$('.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
|
||||
|
||||
isScrolledToBottom: ->
|
||||
|
@ -557,7 +600,8 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
return if !@maxTextLength
|
||||
return if !@warningTextLength
|
||||
params = @params()
|
||||
textLength = @maxTextLength - params.body.length
|
||||
textLength = App.Utils.textLengthWithUrl(params.body)
|
||||
textLength = @maxTextLength - textLength
|
||||
className = switch
|
||||
when textLength < 0 then 'label-danger'
|
||||
when textLength < @warningTextLength then 'label-warning'
|
||||
|
|
|
@ -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')()
|
|
@ -9,18 +9,32 @@ class App.TicketZoomSidebar extends App.ObserverController
|
|||
if backend && backend.reload
|
||||
backend.reload(args)
|
||||
|
||||
commit: (args) =>
|
||||
for key, backend of @sidebarBackends
|
||||
if backend && backend.commit
|
||||
backend.commit(args)
|
||||
|
||||
render: (ticket) =>
|
||||
@sidebarBackends = {}
|
||||
@sidebarBackends ||= {}
|
||||
@sidebarItems = []
|
||||
sidebarBackends = App.Config.get('TicketZoomSidebar')
|
||||
keys = _.keys(sidebarBackends).sort()
|
||||
for key in keys
|
||||
@sidebarBackends[key] = new sidebarBackends[key](
|
||||
ticket: ticket
|
||||
taskGet: @taskGet
|
||||
formMeta: @formMeta
|
||||
markForm: @markForm
|
||||
)
|
||||
if !@sidebarBackends[key] || !@sidebarBackends[key].reload
|
||||
@sidebarBackends[key] = new sidebarBackends[key](
|
||||
ticket: ticket
|
||||
query: @query
|
||||
taskGet: @taskGet
|
||||
formMeta: @formMeta
|
||||
markForm: @markForm
|
||||
)
|
||||
else
|
||||
@sidebarBackends[key].reload(
|
||||
params: @params
|
||||
query: @query
|
||||
formMeta: @formMeta
|
||||
markForm: @markForm
|
||||
)
|
||||
item = @sidebarBackends[key].sidebarItem()
|
||||
if item
|
||||
@sidebarItems.push item
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class SidebarCustomer extends App.Controller
|
||||
sidebarItem: =>
|
||||
return if !@permissionCheck('ticket.agent')
|
||||
{
|
||||
items = {
|
||||
head: 'Customer'
|
||||
name: 'customer'
|
||||
icon: 'person'
|
||||
|
@ -11,14 +11,16 @@ class SidebarCustomer extends App.Controller
|
|||
name: 'customer-change'
|
||||
callback: @changeCustomer
|
||||
},
|
||||
{
|
||||
title: 'Edit Customer'
|
||||
name: 'customer-edit'
|
||||
callback: @editCustomer
|
||||
},
|
||||
]
|
||||
callback: @showCustomer
|
||||
}
|
||||
return items if @ticket && @ticket.customer_id == 1
|
||||
items.actions.push {
|
||||
title: 'Edit Customer'
|
||||
name: 'customer-edit'
|
||||
callback: @editCustomer
|
||||
}
|
||||
items
|
||||
|
||||
showCustomer: (el) =>
|
||||
@el = el
|
||||
|
|
|
@ -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')
|
|
@ -1,5 +1,6 @@
|
|||
class SidebarOrganization extends App.Controller
|
||||
sidebarItem: =>
|
||||
return if !@permissionCheck('ticket.agent')
|
||||
return if !@ticket.organization_id
|
||||
{
|
||||
head: 'Organization'
|
||||
|
|
|
@ -69,7 +69,7 @@ class SidebarTicket extends App.Controller
|
|||
if args.tags
|
||||
@tagWidget.reload(args.tags)
|
||||
if args.tagAdd
|
||||
@tagWidget.add(args.tagAdd)
|
||||
@tagWidget.add(args.tagAdd, args.source)
|
||||
if args.tagRemove
|
||||
@tagWidget.remove(args.tagRemove)
|
||||
|
||||
|
|
|
@ -7,6 +7,12 @@ class App.WidgetAvatar extends App.ObserverController
|
|||
email: true
|
||||
image: 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
|
||||
|
||||
render: (user) =>
|
||||
|
|
|
@ -5,10 +5,10 @@ class Widget
|
|||
banner = """
|
||||
|
|
||||
| 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('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() // current settings
|
||||
|
|
|
@ -10,9 +10,9 @@ class Widget
|
|||
|
|
||||
| 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;')
|
||||
|
|
|
@ -25,6 +25,7 @@ class App.HttpLog extends App.Controller
|
|||
render: =>
|
||||
@html App.view('widget/http_log')(
|
||||
records: @records
|
||||
description: @description
|
||||
)
|
||||
|
||||
show: (e) =>
|
||||
|
|
|
@ -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
|
|
@ -86,16 +86,16 @@ class App.WidgetTag extends App.Controller
|
|||
return
|
||||
@add(item)
|
||||
|
||||
add: (items) =>
|
||||
add: (items, source = '') =>
|
||||
for item in items.split(',')
|
||||
item = item.trim()
|
||||
@addItem(item)
|
||||
@addItem(item, source)
|
||||
|
||||
addItem: (item) =>
|
||||
addItem: (item, source = '') =>
|
||||
if _.contains(@localTags, item)
|
||||
@render()
|
||||
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
|
||||
@render()
|
||||
App[@object_type].tagAdd(@object.id, item)
|
||||
|
|
|
@ -8,7 +8,7 @@ class App.WidgetTextModule extends App.Controller
|
|||
# remember instances
|
||||
@bindElements = []
|
||||
if @selector
|
||||
@bindElements = @$( @selector ).textmodule()
|
||||
@bindElements = @$(@selector).textmodule()
|
||||
else
|
||||
if @el.attr('contenteditable')
|
||||
@bindElements = @el.textmodule()
|
||||
|
|
57
app/assets/javascripts/app/lib/app_init/queue_manager.coffee
Normal file
57
app/assets/javascripts/app/lib/app_init/queue_manager.coffee
Normal 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
|
|
@ -5,9 +5,9 @@ class App._CollectionSingletonBase
|
|||
constructor: ->
|
||||
@callbacks = {}
|
||||
@counter = 0
|
||||
|
||||
@key = "collection-#{@event}"
|
||||
# read from cache
|
||||
cache = App.SessionStorage.get("collection-#{@event}")
|
||||
cache = App.SessionStorage.get(@key)
|
||||
if cache
|
||||
@set(cache)
|
||||
|
||||
|
@ -73,6 +73,9 @@ class App._CollectionSingletonBase
|
|||
|
||||
callback: (data) =>
|
||||
for counter, attr of @callbacks
|
||||
attr.callback(data)
|
||||
if attr.one
|
||||
delete @callbacks[counter]
|
||||
callback = ->
|
||||
attr.callback(data)
|
||||
if attr.one
|
||||
delete @callbacks[counter]
|
||||
App.QueueManager.add(@key, callback)
|
||||
App.QueueManager.run(@key)
|
||||
|
|
|
@ -113,6 +113,10 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
@createToken name, objectId
|
||||
else
|
||||
if object.email
|
||||
|
||||
# quote name for special character
|
||||
if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/)
|
||||
name = "\"#{name}\""
|
||||
name += " <#{object.email}>"
|
||||
|
||||
@objectSelect.val(name)
|
||||
|
@ -390,14 +394,14 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
properties:
|
||||
translateX: 0
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
|
||||
# fade out list
|
||||
@recipientList.velocity
|
||||
properties:
|
||||
translateX: '-100%'
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
complete: => @recipientList.height(@organizationList.height())
|
||||
|
||||
hideOrganizationMembers: (e) =>
|
||||
|
@ -413,7 +417,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
properties:
|
||||
translateX: 0
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
|
||||
# reset list height
|
||||
@recipientList.height('')
|
||||
|
@ -423,7 +427,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
properties:
|
||||
translateX: '100%'
|
||||
options:
|
||||
speed: 300
|
||||
duration: 240
|
||||
complete: => @organizationList.addClass('hide')
|
||||
|
||||
newObject: (e) ->
|
||||
|
|
|
@ -6,25 +6,25 @@ class App.ClipBoard
|
|||
_instance ?= new _Singleton
|
||||
_instance.bind(el)
|
||||
|
||||
@getSelected: ->
|
||||
@getSelected: (type) ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _Singleton
|
||||
_instance.getSelected()
|
||||
_instance.getSelected(type)
|
||||
|
||||
@getSelectedLast: ->
|
||||
@getSelectedLast: (type) ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _Singleton
|
||||
_instance.getSelectedLast()
|
||||
_instance.getSelectedLast(type)
|
||||
|
||||
@getPosition: (el) ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _Singleton
|
||||
_instance.getPosition(el)
|
||||
|
||||
@setPosition: ( el, pos ) ->
|
||||
@setPosition: (el, pos) ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _Singleton
|
||||
_instance.setPosition( el, pos )
|
||||
_instance.setPosition(el, pos)
|
||||
|
||||
@keycode: (code) ->
|
||||
if _instance == undefined
|
||||
|
@ -33,54 +33,68 @@ class App.ClipBoard
|
|||
|
||||
class _Singleton
|
||||
constructor: ->
|
||||
@selection = ''
|
||||
@selectionLast = ''
|
||||
@selection =
|
||||
html: ''
|
||||
text: ''
|
||||
@selectionLast =
|
||||
html: ''
|
||||
text: ''
|
||||
|
||||
# bind to fill selected text into
|
||||
bind: (el) ->
|
||||
$(el).bind('mouseup', =>
|
||||
|
||||
# check selection on mouse up
|
||||
@selection = @_getSelected()
|
||||
if @selection
|
||||
@selectionLast = @selection
|
||||
# check selection on mouse up
|
||||
$(el).bind('mouseup', =>
|
||||
@_updateSelection()
|
||||
)
|
||||
$(el).bind('keyup', (e) =>
|
||||
|
||||
# check selection on sonder key
|
||||
if e.keyCode == 91
|
||||
@selection = @_getSelected()
|
||||
if @selection
|
||||
@selectionLast = @selection
|
||||
@_updateSelection()
|
||||
|
||||
# check selection of arrow keys
|
||||
if e.keyCode == 37 || e.keyCode == 38 || e.keyCode == 39 || e.keyCode == 40
|
||||
@selection = @_getSelected()
|
||||
if @selection
|
||||
@selectionLast = @selection
|
||||
@_updateSelection()
|
||||
)
|
||||
|
||||
_updateSelection: =>
|
||||
for key in ['html', 'text']
|
||||
@selection[key] = @_getSelected(key)
|
||||
if @selection[key]
|
||||
@selectionLast[key] = @selection[key]
|
||||
|
||||
# get cross browser selected string
|
||||
_getSelected: ->
|
||||
_getSelected: (type) ->
|
||||
text = ''
|
||||
html = ''
|
||||
if window.getSelection
|
||||
text = window.getSelection()
|
||||
sel = window.getSelection()
|
||||
text = sel.toString()
|
||||
else if document.getSelection
|
||||
text = document.getSelection()
|
||||
sel = document.getSelection()
|
||||
text = sel.toString()
|
||||
else if document.selection
|
||||
text = document.selection.createRange().text
|
||||
if text
|
||||
# text = text.toString().trim()
|
||||
text = $.trim( text.toString() )
|
||||
text
|
||||
sel = document.selection.createRange()
|
||||
text = sel.text
|
||||
if type is 'text'
|
||||
return $.trim(text.toString()) if 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
|
||||
getSelected: ->
|
||||
@selection
|
||||
getSelected: (type) ->
|
||||
@selection[type]
|
||||
|
||||
# get latest selection
|
||||
getSelectedLast: ->
|
||||
@selectionLast
|
||||
getSelectedLast: (type) ->
|
||||
@selectionLast[type]
|
||||
|
||||
getPosition: (el) ->
|
||||
pos = 0
|
||||
|
@ -104,13 +118,13 @@ class _Singleton
|
|||
# IE Support
|
||||
if el.setSelectionRange
|
||||
el.focus()
|
||||
el.setSelectionRange( pos, pos )
|
||||
el.setSelectionRange(pos, pos)
|
||||
|
||||
# Firefox support
|
||||
else if el.createTextRange
|
||||
range = el.createTextRange()
|
||||
range.collapse(true)
|
||||
range.moveEnd( 'character', pos )
|
||||
range.moveEnd('character', pos)
|
||||
range.moveStart('character', pos)
|
||||
range.select()
|
||||
|
||||
|
|
|
@ -33,6 +33,11 @@ class App.ColumnSelect extends Spine.Controller
|
|||
@select @pickedValue
|
||||
, 300, {trailing: false}
|
||||
|
||||
if @attribute.onChange
|
||||
@shadow.on('change', =>
|
||||
@attribute.onChange(@shadow.val())
|
||||
)
|
||||
|
||||
render: ->
|
||||
@values = []
|
||||
_.each @options.attribute.options, (option) =>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
class App.Delay
|
||||
_instance = undefined
|
||||
|
||||
@set: (callback, timeout, key, level) ->
|
||||
@set: (callback, timeout, key, level, queue) ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _delaySingleton
|
||||
_instance.set(callback, timeout, key, level)
|
||||
_instance.set(callback, timeout, key, level, queue)
|
||||
|
||||
@clear: (key, level) ->
|
||||
if _instance == undefined
|
||||
|
@ -21,6 +21,11 @@ class App.Delay
|
|||
_instance ?= new _delaySingleton
|
||||
_instance.reset()
|
||||
|
||||
@count: ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _intervalSingleton
|
||||
_instance.count()
|
||||
|
||||
@_all: ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _delaySingleton
|
||||
|
@ -32,7 +37,7 @@ class _delaySingleton extends Spine.Module
|
|||
constructor: ->
|
||||
@levelStack = {}
|
||||
|
||||
set: (callback, timeout, key, level) =>
|
||||
set: (callback, timeout, key, level, queue) =>
|
||||
|
||||
if !level
|
||||
level = '_all'
|
||||
|
@ -44,11 +49,15 @@ class _delaySingleton extends Spine.Module
|
|||
key = Math.floor(Math.random() * 99999)
|
||||
|
||||
# setTimeout
|
||||
@log 'debug', 'set', key, timeout, level, callback
|
||||
call = =>
|
||||
@log 'debug', 'set', key, timeout, level, callback, queue
|
||||
localCallback = =>
|
||||
@clear(key, level)
|
||||
callback()
|
||||
delay_id = setTimeout(call, timeout)
|
||||
if queue
|
||||
App.QueueManager.add('delay', callback)
|
||||
App.QueueManager.run('delay')
|
||||
else
|
||||
callback()
|
||||
delay_id = setTimeout(localCallback, timeout)
|
||||
|
||||
# remember all delays
|
||||
if !@levelStack[level]
|
||||
|
@ -93,6 +102,13 @@ class _delaySingleton extends Spine.Module
|
|||
@levelStack[level] = {}
|
||||
true
|
||||
|
||||
count: =>
|
||||
return 0 if !@levelStack
|
||||
count = 0
|
||||
for levelName, levelValue of @levelStack
|
||||
count += Object.keys(levelValue).length
|
||||
count
|
||||
|
||||
_all: =>
|
||||
@levelStack
|
||||
|
||||
|
|
|
@ -30,6 +30,11 @@ class App.i18n
|
|||
_instance ?= new _i18nSingleton()
|
||||
_instance.date(args, offset)
|
||||
|
||||
@dir: ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _i18nSingleton()
|
||||
_instance.dir()
|
||||
|
||||
@get: ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _i18nSingleton()
|
||||
|
@ -88,6 +93,10 @@ class _i18nSingleton extends Spine.Module
|
|||
@_notTranslated = {}
|
||||
@dateFormat = 'yyyy-mm-dd'
|
||||
@timestampFormat = 'yyyy-mm-dd HH:MM'
|
||||
@dirToSet = 'ltr'
|
||||
|
||||
dir: ->
|
||||
@dirToSet
|
||||
|
||||
get: ->
|
||||
@locale
|
||||
|
@ -96,12 +105,15 @@ class _i18nSingleton extends Spine.Module
|
|||
|
||||
# prepare locale
|
||||
localeToSet = localeToSet.toLowerCase()
|
||||
@dirToSet = 'ltr'
|
||||
|
||||
# check if locale exists
|
||||
localeFound = false
|
||||
locales = App.Locale.all()
|
||||
for locale in locales
|
||||
if locale.locale is localeToSet
|
||||
localeToSet = locale.locale
|
||||
@dirToSet = locale.dir
|
||||
localeFound = true
|
||||
|
||||
# try aliases
|
||||
|
@ -109,6 +121,8 @@ class _i18nSingleton extends Spine.Module
|
|||
for locale in locales
|
||||
if locale.alias is localeToSet
|
||||
localeToSet = locale.locale
|
||||
@dirToSet = locale.dir
|
||||
localeFound = true
|
||||
|
||||
# if no locale and no alias was found, try to find correct one
|
||||
if !localeFound
|
||||
|
@ -118,15 +132,9 @@ class _i18nSingleton extends Spine.Module
|
|||
for locale in locales
|
||||
if locale.alias is localeToSet
|
||||
localeToSet = locale.locale
|
||||
@dirToSet = locale.dir
|
||||
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
|
||||
return if localeToSet is @locale
|
||||
|
||||
|
@ -136,8 +144,9 @@ class _i18nSingleton extends Spine.Module
|
|||
# set if not translated should be logged
|
||||
@_notTranslatedLog = @notTranslatedFeatureEnabled(@locale)
|
||||
|
||||
# set lang attribute of html tag
|
||||
$('html').prop('lang', @locale.substr(0, 2) )
|
||||
# set lang and dir attribute of html tag
|
||||
$('html').prop('lang', localeToSet.substr(0, 2))
|
||||
$('html').prop('dir', @dirToSet)
|
||||
|
||||
@mapString = {}
|
||||
App.Ajax.request(
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
class App.Interval
|
||||
_instance = undefined
|
||||
|
||||
@set: (callback, timeout, key, level) ->
|
||||
@set: (callback, timeout, key, level, queue) ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _intervalSingleton
|
||||
_instance.set(callback, timeout, key, level)
|
||||
_instance.set(callback, timeout, key, level, queue)
|
||||
|
||||
@clear: (key, level) ->
|
||||
if _instance == undefined
|
||||
|
@ -21,6 +21,11 @@ class App.Interval
|
|||
_instance ?= new _intervalSingleton
|
||||
_instance.reset()
|
||||
|
||||
@count: ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _intervalSingleton
|
||||
_instance.count()
|
||||
|
||||
@_all: ->
|
||||
if _instance == undefined
|
||||
_instance ?= new _intervalSingleton
|
||||
|
@ -32,7 +37,7 @@ class _intervalSingleton extends Spine.Module
|
|||
constructor: ->
|
||||
@levelStack = {}
|
||||
|
||||
set: (callback, timeout, key, level) =>
|
||||
set: (callback, timeout, key, level, queue) =>
|
||||
|
||||
if !level
|
||||
level = '_all'
|
||||
|
@ -44,9 +49,15 @@ class _intervalSingleton extends Spine.Module
|
|||
key = Math.floor(Math.random() * 99999)
|
||||
|
||||
# setTimeout
|
||||
@log 'debug', 'set', key, timeout, level, callback
|
||||
callback()
|
||||
interval_id = setInterval(callback, timeout)
|
||||
@log 'debug', 'set', key, timeout, level, callback, queue
|
||||
localCallback = ->
|
||||
if queue
|
||||
App.QueueManager.add('interval', callback)
|
||||
App.QueueManager.run('interval')
|
||||
else
|
||||
callback()
|
||||
localCallback()
|
||||
interval_id = setInterval(localCallback, timeout)
|
||||
|
||||
# remember all interval
|
||||
if !@levelStack[level]
|
||||
|
@ -91,5 +102,12 @@ class _intervalSingleton extends Spine.Module
|
|||
@levelStack[level] = {}
|
||||
true
|
||||
|
||||
count: =>
|
||||
return 0 if !@levelStack
|
||||
count = 0
|
||||
for levelName, levelValue of @levelStack
|
||||
count += Object.keys(levelValue).length
|
||||
count
|
||||
|
||||
_all: =>
|
||||
@levelStack
|
||||
|
|
|
@ -71,7 +71,10 @@ class _Singleton
|
|||
callback: (view, data) =>
|
||||
for counter, meta of @callbacks
|
||||
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
|
||||
_instance = new _Singleton
|
||||
|
|
|
@ -38,11 +38,16 @@ class App.PrettyDate
|
|||
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
month = months[created.getMonth()]
|
||||
|
||||
# for less than 7 days
|
||||
if diff < (60 * 60 * 24 * 7)
|
||||
# for less than 6 days
|
||||
# weekday HH::MM
|
||||
if diff < (60 * 60 * 24 * 6)
|
||||
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)}"
|
||||
# if it was the year before
|
||||
# weekday YYYY-MM-DD HH::MM
|
||||
else
|
||||
string = "#{App.i18n.translateInline(weekday)} #{App.i18n.translateTimestamp(time)}"
|
||||
if escalation
|
||||
|
|
|
@ -1,19 +1,26 @@
|
|||
class App.SearchableSelect extends Spine.Controller
|
||||
|
||||
events:
|
||||
'input .js-input': 'onInput'
|
||||
'blur .js-input': 'onBlur'
|
||||
'focus .js-input': 'onFocus'
|
||||
'click .js-option': 'selectItem'
|
||||
'mouseenter .js-option': 'highlightItem'
|
||||
'shown.bs.dropdown': 'onDropdownShown'
|
||||
'hidden.bs.dropdown': 'onDropdownHidden'
|
||||
'input .js-input': 'onInput'
|
||||
'blur .js-input': 'onBlur'
|
||||
'focus .js-input': 'onFocus'
|
||||
'click .js-option': 'selectItem'
|
||||
'click .js-enter': 'navigateIn'
|
||||
'click .js-back': 'navigateOut'
|
||||
'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:
|
||||
'.js-option': 'option_items'
|
||||
'.js-dropdown': 'dropdown'
|
||||
'.js-option, .js-enter': 'optionItems'
|
||||
'.js-input': 'input'
|
||||
'.js-shadow': 'shadowInput'
|
||||
'.js-optionsList': 'optionsList'
|
||||
'.js-optionsSubmenu': 'optionsSubmenu'
|
||||
'.js-autocomplete-invisible': 'invisiblePart'
|
||||
'.js-autocomplete-visible': 'visiblePart'
|
||||
|
||||
|
@ -27,32 +34,99 @@ class App.SearchableSelect extends Spine.Controller
|
|||
@render()
|
||||
|
||||
render: ->
|
||||
firstSelected = _.find @options.attribute.options, (option) -> option.selected
|
||||
firstSelected = _.find @attribute.options, (option) -> option.selected
|
||||
|
||||
if firstSelected
|
||||
@options.attribute.valueName = firstSelected.name
|
||||
@options.attribute.value = firstSelected.value
|
||||
else if @options.attribute.unknown && @options.attribute.value
|
||||
@options.attribute.valueName = @options.attribute.value
|
||||
@attribute.valueName = firstSelected.name
|
||||
@attribute.value = firstSelected.value
|
||||
else if @attribute.unknown && @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')
|
||||
options: @options.attribute.options
|
||||
@html App.view('generic/searchable_select')
|
||||
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: =>
|
||||
@input.on 'click', @stopPropagation
|
||||
@highlightFirst()
|
||||
$(document).on 'keydown.searchable_select', @navigate
|
||||
if @level > 0
|
||||
@showSubmenu(@currentMenu)
|
||||
@isOpen = true
|
||||
|
||||
onDropdownHidden: =>
|
||||
@input.off 'click', @stopPropagation
|
||||
@option_items.removeClass '.is-active'
|
||||
@unhighlightCurrentItem()
|
||||
$(document).off 'keydown.searchable_select'
|
||||
@isOpen = false
|
||||
|
||||
onKeyUp: =>
|
||||
return if @input.val().trim() isnt ''
|
||||
@shadowInput.val('')
|
||||
|
||||
toggle: =>
|
||||
@currentItem = null
|
||||
@$('[data-toggle="dropdown"]').dropdown('toggle')
|
||||
|
||||
stopPropagation: (event) ->
|
||||
|
@ -62,8 +136,8 @@ class App.SearchableSelect extends Spine.Controller
|
|||
switch event.keyCode
|
||||
when 40 then @nudge event, 1 # down
|
||||
when 38 then @nudge event, -1 # up
|
||||
when 39 then @fillWithAutocompleteSuggestion event # right
|
||||
when 37 then @fillWithAutocompleteSuggestion event # left
|
||||
when 39 then @autocompleteOrNavigateIn event # right
|
||||
when 37 then @autocompleteOrNavigateOut event # left
|
||||
when 13 then @onEnter event
|
||||
when 27 then @onEscape()
|
||||
when 9 then @onTab event
|
||||
|
@ -71,12 +145,20 @@ class App.SearchableSelect extends Spine.Controller
|
|||
onEscape: ->
|
||||
@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) ->
|
||||
return @toggle() if not @isOpen
|
||||
|
||||
options = @getCurrentOptions()
|
||||
|
||||
event.preventDefault()
|
||||
visibleOptions = @option_items.not('.is-hidden')
|
||||
highlightedItem = @option_items.filter('.is-active')
|
||||
visibleOptions = options.not('.is-hidden')
|
||||
highlightedItem = options.filter('.is-active')
|
||||
currentPosition = visibleOptions.index(highlightedItem)
|
||||
|
||||
currentPosition += direction
|
||||
|
@ -84,10 +166,24 @@ class App.SearchableSelect extends Spine.Controller
|
|||
return if currentPosition < 0
|
||||
return if currentPosition > visibleOptions.size() - 1
|
||||
|
||||
@option_items.removeClass('is-active')
|
||||
visibleOptions.eq(currentPosition).addClass('is-active')
|
||||
@unhighlightCurrentItem()
|
||||
@currentItem = visibleOptions.eq(currentPosition)
|
||||
@currentItem.addClass('is-active')
|
||||
@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) ->
|
||||
if !@suggestion
|
||||
return
|
||||
|
@ -124,16 +220,101 @@ class App.SearchableSelect extends Spine.Controller
|
|||
@invisiblePart.text('')
|
||||
|
||||
selectItem: (event) ->
|
||||
return if !event.currentTarget.textContent
|
||||
@input.val event.currentTarget.textContent.trim()
|
||||
@input.trigger('change')
|
||||
@shadowInput.val event.currentTarget.getAttribute('data-value')
|
||||
@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) ->
|
||||
return if not @isOpen
|
||||
event.preventDefault()
|
||||
|
||||
onEnter: (event) ->
|
||||
if @currentItem
|
||||
if @currentItem.hasClass('js-back')
|
||||
return @navigateOut(event)
|
||||
|
||||
@clearAutocomplete()
|
||||
|
||||
if not @isOpen
|
||||
|
@ -144,15 +325,22 @@ class App.SearchableSelect extends Spine.Controller
|
|||
|
||||
event.preventDefault()
|
||||
|
||||
selected = @option_items.filter('.is-active')
|
||||
if selected.length || !@options.attribute.unknown
|
||||
valueName = selected.text().trim()
|
||||
value = selected.attr('data-value')
|
||||
if @currentItem || !@attribute.unknown
|
||||
valueName = @currentItem.text().trim()
|
||||
value = @currentItem.attr('data-value')
|
||||
@input.val valueName
|
||||
@shadowInput.val value
|
||||
|
||||
@input.trigger('change')
|
||||
@shadowInput.trigger('change')
|
||||
|
||||
if @currentItem
|
||||
if @currentItem.hasClass('js-enter')
|
||||
@navigateIn(event)
|
||||
@currentItem = null
|
||||
return
|
||||
@currentItem = null
|
||||
|
||||
@toggle()
|
||||
|
||||
onBlur: ->
|
||||
|
@ -169,32 +357,46 @@ class App.SearchableSelect extends Spine.Controller
|
|||
@query = @input.val()
|
||||
@filterByQuery @query
|
||||
|
||||
if @options.attribute.unknown
|
||||
if @attribute.unknown
|
||||
@shadowInput.val @query
|
||||
|
||||
filterByQuery: (query) ->
|
||||
query = escapeRegExp(query)
|
||||
regex = new RegExp(query.split(' ').join('.*'), 'i')
|
||||
|
||||
@option_items
|
||||
@optionsList.addClass 'is-filtered'
|
||||
|
||||
@optionItems
|
||||
.addClass 'is-hidden'
|
||||
.filter ->
|
||||
@textContent.match(regex)
|
||||
.removeClass 'is-hidden'
|
||||
|
||||
if @options.attribute.unknown && @option_items.length == @option_items.filter('.is-hidden').length
|
||||
@option_items.removeClass 'is-hidden'
|
||||
@option_items.removeClass 'is-active'
|
||||
if !query
|
||||
@optionItems.filter('.is-child').addClass 'is-hidden'
|
||||
|
||||
# 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
|
||||
@highlightFirst(true)
|
||||
|
||||
highlightFirst: (autocomplete) ->
|
||||
first = @option_items.removeClass('is-active').not('.is-hidden').first()
|
||||
first.addClass 'is-active'
|
||||
@unhighlightCurrentItem()
|
||||
@currentItem = @getCurrentOptions().not('.is-hidden').first()
|
||||
@currentItem.addClass 'is-active'
|
||||
|
||||
if autocomplete
|
||||
@autocomplete first.attr('data-value'), first.text().trim()
|
||||
@autocomplete @currentItem.attr('data-value'), @currentItem.text().trim()
|
||||
|
||||
highlightItem: (event) =>
|
||||
@option_items.removeClass('is-active')
|
||||
$(event.currentTarget).addClass('is-active')
|
||||
@unhighlightCurrentItem()
|
||||
@currentItem = $(event.currentTarget)
|
||||
@currentItem.addClass('is-active')
|
||||
|
||||
unhighlightCurrentItem: ->
|
||||
return if !@currentItem
|
||||
@currentItem.removeClass('is-active')
|
||||
@currentItem = null
|
||||
|
|
|
@ -1,5 +1,80 @@
|
|||
# coffeelint: disable=no_unnecessary_double_quotes
|
||||
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)
|
||||
@textCleanup: (ascii) ->
|
||||
|
@ -49,10 +124,12 @@ class App.Utils
|
|||
@linkify: (string) ->
|
||||
window.linkify(string)
|
||||
|
||||
# htmlEscapedAndLinkified = App.Utils.linkify(rawText)
|
||||
# htmlEscapedAndPhoneified = App.Utils.phoneify(rawText)
|
||||
@phoneify: (string) ->
|
||||
string = string.replace(/\s+/g, '')
|
||||
"tel://#{encodeURIComponent(string)}"
|
||||
return string if _.isEmpty(string)
|
||||
string = string.replace(/[^0-9,\+,#,\*]+/g, '')
|
||||
.replace(/(.)\+/, '$1')
|
||||
"tel:#{string}"
|
||||
|
||||
# wrappedText = App.Utils.wrap(rawText, maxLineLength)
|
||||
@wrap: (ascii, max = 82) ->
|
||||
|
@ -125,6 +202,7 @@ class App.Utils
|
|||
child = el.firstChild
|
||||
break if !child
|
||||
break if child.nodeType isnt 1 || child.tagName isnt 'BR'
|
||||
break if !child.remove
|
||||
child.remove()
|
||||
|
||||
loop
|
||||
|
@ -133,6 +211,7 @@ class App.Utils
|
|||
child = el.lastChild
|
||||
break if !child
|
||||
break if child.nodeType isnt 1 || child.tagName isnt 'BR'
|
||||
break if !child.remove
|
||||
child.remove()
|
||||
|
||||
# true|false = App.Utils.htmlLastLineEmpty(element)
|
||||
|
@ -155,12 +234,12 @@ class App.Utils
|
|||
@_removeWordMarkup(html)
|
||||
|
||||
# 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()
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -172,20 +251,19 @@ class App.Utils
|
|||
# remove comments
|
||||
@_removeComments(html)
|
||||
|
||||
# remove style and class
|
||||
if parent
|
||||
@_removeAttributes(html)
|
||||
|
||||
# remove work markup
|
||||
@_removeWordMarkup(html)
|
||||
|
||||
# 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()
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
|
@ -197,9 +275,6 @@ class App.Utils
|
|||
# remove comments
|
||||
@_removeComments(html)
|
||||
|
||||
# remove style and class
|
||||
@_removeAttributes(html)
|
||||
|
||||
# remove work markup
|
||||
@_removeWordMarkup(html)
|
||||
|
||||
|
@ -230,6 +305,9 @@ class App.Utils
|
|||
# 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()
|
||||
|
||||
# remove style and class
|
||||
@_cleanAttributes(html)
|
||||
|
||||
html
|
||||
|
||||
@_checkTypeOf: (item) ->
|
||||
|
@ -250,26 +328,64 @@ class App.Utils
|
|||
catch err
|
||||
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
|
||||
html.find('*')
|
||||
.removeAttr('style')
|
||||
.removeAttr('class')
|
||||
.removeAttr('title')
|
||||
.removeAttr('lang')
|
||||
.removeAttr('type')
|
||||
.removeAttr('id')
|
||||
.removeAttr('wrap')
|
||||
.removeAttrs(/data-/)
|
||||
html.each((index, element) => @_cleanAttribute(element) )
|
||||
html.find('*').each((index, element) => @_cleanAttribute(element) )
|
||||
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('title')
|
||||
.removeAttr('lang')
|
||||
.removeAttr('type')
|
||||
.removeAttr('align')
|
||||
.removeAttr('id')
|
||||
.removeAttr('wrap')
|
||||
.removeAttr('title')
|
||||
.removeAttrs(/data-/)
|
||||
|
||||
@_removeAttributes: (html, parent = true) ->
|
||||
if parent
|
||||
html.each((index, element) => @_removeAttribute(element) )
|
||||
html.find('*').each((index, element) => @_removeAttribute(element) )
|
||||
html
|
||||
|
||||
@_removeComments: (html) ->
|
||||
|
@ -535,6 +651,7 @@ class App.Utils
|
|||
# textReplaced = App.Utils.replaceTags( template, { user: { firstname: 'Bob', lastname: 'Smith' } } )
|
||||
@replaceTags: (template, objects) ->
|
||||
template = template.replace( /#\{\s{0,2}(.+?)\s{0,2}\}/g, (index, key) ->
|
||||
key = key.replace(/<.+?>/g, '')
|
||||
levels = key.split(/\./)
|
||||
dataRef = objects
|
||||
for level in levels
|
||||
|
@ -781,3 +898,10 @@ class App.Utils
|
|||
result = newOrderMethod(a, b, applyOrder)
|
||||
return false if !result
|
||||
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
|
||||
|
|
|
@ -70,8 +70,7 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
|
|||
options.push data
|
||||
|
||||
# fill template with gathered options
|
||||
@optionsList.html App.view('generic/searchable_select_options')
|
||||
options: options
|
||||
@optionsList.html @renderOptions options
|
||||
|
||||
# refresh elements
|
||||
@refreshElements()
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -88,7 +88,7 @@
|
|||
// handle enter
|
||||
this.$element.on('keydown', function (e) {
|
||||
_this.log('keydown', e.keyCode)
|
||||
if ( _this.preventInput ) {
|
||||
if (_this.preventInput) {
|
||||
this.log('preventInput', _this.preventInput)
|
||||
return
|
||||
}
|
||||
|
@ -97,18 +97,29 @@
|
|||
if (e.keyCode === 13) {
|
||||
|
||||
// disbale multi line
|
||||
if ( !_this.options.multiline ) {
|
||||
if (!_this.options.multiline) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
// break <blockquote> after enter on empty line
|
||||
sel = window.getSelection()
|
||||
node = $(sel.anchorNode)
|
||||
if (node.parent().is('blockquote')) {
|
||||
if (sel) {
|
||||
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()
|
||||
document.execCommand('Insertparagraph')
|
||||
document.execCommand('Outdent')
|
||||
_this.paste('<br><br>')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -237,7 +248,7 @@
|
|||
|
||||
// limit check
|
||||
if ( !_this.allowKey(e) ) {
|
||||
if ( !_this.maxLengthOk( 1 ) ) {
|
||||
if ( !_this.maxLengthOk(1) ) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
@ -254,6 +265,9 @@
|
|||
if (e.clipboardData) { // ie
|
||||
clipboardData = e.clipboardData
|
||||
}
|
||||
else if (window.clipboardData) { // ie
|
||||
clipboardData = window.clipboardData
|
||||
}
|
||||
else if (e.originalEvent.clipboardData) { // other browsers
|
||||
clipboardData = e.originalEvent.clipboardData
|
||||
}
|
||||
|
@ -292,7 +306,7 @@
|
|||
else {
|
||||
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
|
||||
}
|
||||
document.execCommand('insertHTML', false, img)
|
||||
_this.paste(img)
|
||||
}
|
||||
|
||||
// resize if to big
|
||||
|
@ -307,15 +321,23 @@
|
|||
}
|
||||
|
||||
// check existing + paste text for limit
|
||||
var text = clipboardData.getData('text/html')
|
||||
var docType = 'html'
|
||||
if (!text || text.length === 0) {
|
||||
docType = 'text'
|
||||
text = clipboardData.getData('text/plain')
|
||||
var text, docType
|
||||
try {
|
||||
text = clipboardData.getData('text/html')
|
||||
docType = 'html'
|
||||
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) {
|
||||
docType = 'text2'
|
||||
text = clipboardData.getData('text')
|
||||
catch (e) {
|
||||
console.log('Sorry, can\'t insert markup because browser is not supporting it.')
|
||||
docType = 'text3'
|
||||
text = clipboardData.getData('text')
|
||||
}
|
||||
_this.log('paste', docType, text)
|
||||
|
||||
|
@ -355,7 +377,8 @@
|
|||
// cleanup
|
||||
text = App.Utils.removeEmptyLines(text)
|
||||
_this.log('insert', text)
|
||||
document.execCommand('insertHTML', false, text)
|
||||
|
||||
_this.paste(text)
|
||||
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 () {
|
||||
if (!$.data(this, 'plugin_' + pluginName)) {
|
||||
$.data(this, 'plugin_' + pluginName,
|
||||
|
@ -537,6 +583,9 @@
|
|||
// get correct val if textbox
|
||||
$.fn.ceg = function() {
|
||||
var plugin = $.data(this[0], 'plugin_' + pluginName)
|
||||
if (!plugin) {
|
||||
return
|
||||
}
|
||||
return plugin.value()
|
||||
}
|
||||
|
||||
|
|
|
@ -43,17 +43,21 @@
|
|||
|
||||
this.$element.on('keydown', function (e) {
|
||||
|
||||
// esc
|
||||
if (e.keyCode === 27) {
|
||||
_this.close()
|
||||
}
|
||||
|
||||
// navigate through item
|
||||
if (_this.isActive()) {
|
||||
|
||||
// esc
|
||||
if (e.keyCode === 27) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
_this.close()
|
||||
return
|
||||
}
|
||||
|
||||
// enter
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
var id = _this.$widget.find('.dropdown-menu li.is-active').data('id')
|
||||
|
||||
// as fallback use hovered element
|
||||
|
@ -72,12 +76,14 @@
|
|||
// arrow keys left/right
|
||||
if (e.keyCode === 37 || e.keyCode === 39) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
// up or down
|
||||
if (e.keyCode === 38 || e.keyCode === 40) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
var active = _this.$widget.find('.dropdown-menu li.is-active')
|
||||
active.removeClass('is-active')
|
||||
|
||||
|
@ -92,6 +98,9 @@
|
|||
|
||||
var menu = _this.$widget.find('.dropdown-menu')
|
||||
|
||||
if (!active.get(0)) {
|
||||
return
|
||||
}
|
||||
if (active.position().top < 0) {
|
||||
// scroll up
|
||||
menu.scrollTop( menu.scrollTop() + active.position().top )
|
||||
|
@ -102,7 +111,11 @@
|
|||
menu.scrollTop( menu.scrollTop() + invisibleHeight )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// esc
|
||||
if (e.keyCode === 27) {
|
||||
_this.close()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -180,14 +193,14 @@
|
|||
Plugin.prototype.renderBase = function() {
|
||||
this.$element.after('<div class="shortcut dropdown"><ul class="dropdown-menu" style="max-height: 200px;"></ul></div>')
|
||||
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))
|
||||
}
|
||||
|
||||
// set height of widget
|
||||
Plugin.prototype.movePosition = function() {
|
||||
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 top = -( widgetHeight + height ) + this._position.top
|
||||
var left = this._position.left - 6
|
||||
|
@ -250,9 +263,21 @@
|
|||
|
||||
// paste some content
|
||||
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()
|
||||
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 {
|
||||
document.execCommand('insertHTML', false, string)
|
||||
|
@ -295,14 +320,7 @@
|
|||
// for chrome, insert space again
|
||||
if (start) {
|
||||
if (spacerChar === ' ') {
|
||||
string = " "
|
||||
if (document.selection) { // IE
|
||||
var range = document.selection.createRange()
|
||||
range.pasteHTML(string)
|
||||
}
|
||||
else {
|
||||
document.execCommand('insertHTML', false, string)
|
||||
}
|
||||
this.paste(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,6 +331,7 @@
|
|||
}
|
||||
|
||||
Plugin.prototype.onEntryClick = function(event) {
|
||||
event.preventDefault()
|
||||
var id = $(event.target).data('id')
|
||||
this.take(id)
|
||||
}
|
||||
|
@ -325,7 +344,7 @@
|
|||
}
|
||||
for (var i = 0; i < this.collection.length; i++) {
|
||||
var item = this.collection[i]
|
||||
if ( item.id == id ) {
|
||||
if (item.id == id) {
|
||||
var content = item.content
|
||||
this.cutInput()
|
||||
this.paste(content)
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
modified by Felix Jan-2014
|
||||
- add this.$body = $(options.container || document.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', this.$element[0].scrollHeight)
|
||||
|
||||
if(App.i18n.dir() == 'rtl'){
|
||||
this.$backdrop.css('right', 'auto')
|
||||
}
|
||||
|
||||
if(this.scrollbarWidth){
|
||||
this.$backdrop.css('width', this.$body.width() - this.scrollbarWidth)
|
||||
}
|
||||
|
@ -251,14 +257,22 @@
|
|||
|
||||
Modal.prototype.adjustDialog = function () {
|
||||
var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight
|
||||
|
||||
this.$element.css({
|
||||
var css = {
|
||||
left: this.$body.offset().left,
|
||||
top: this.$body.offset().top,
|
||||
width: this.$body.width(),
|
||||
paddingLeft: !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 () {
|
||||
|
|
|
@ -32,6 +32,10 @@ class App.Model extends Spine.Model
|
|||
return @title
|
||||
if @subject
|
||||
return @subject
|
||||
if @phone
|
||||
return @phone
|
||||
if @login
|
||||
return @login
|
||||
return '???'
|
||||
|
||||
displayNameLong: ->
|
||||
|
@ -57,6 +61,12 @@ class App.Model extends Spine.Model
|
|||
return @email
|
||||
if @title
|
||||
return @title
|
||||
if @subject
|
||||
return @subject
|
||||
if @phone
|
||||
return @phone
|
||||
if @login
|
||||
return @login
|
||||
return '???'
|
||||
|
||||
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)
|
||||
|
||||
returns
|
||||
|
|
|
@ -35,3 +35,11 @@ class App.Group extends App.Model
|
|||
|
||||
return App.view('avatar_group')
|
||||
cssClass: cssClass.join(' ')
|
||||
|
||||
@accesses: ->
|
||||
read: 'Read'
|
||||
create: 'Create'
|
||||
change: 'Change'
|
||||
delete: 'Delete'
|
||||
overview: 'Overview'
|
||||
full: 'Full'
|
||||
|
|
|
@ -6,7 +6,7 @@ class App.Job extends App.Model
|
|||
{ 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: '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: '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 },
|
||||
|
|
|
@ -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: '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: '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: 'prio', display: 'Prio', readonly: 1 },
|
||||
{
|
||||
|
@ -72,4 +73,4 @@ Sie können auch individuelle Übersichten für einzelne Agenten oder agenten Gr
|
|||
'''
|
||||
|
||||
uiUrl: ->
|
||||
'#ticket/view/' + @link
|
||||
"#ticket/view/#{@link}"
|
||||
|
|
|
@ -6,7 +6,7 @@ class App.PostmasterFilter extends App.Model
|
|||
@configure_attributes = [
|
||||
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 250, 'null': false },
|
||||
{ name: 'channel', display: 'Channel', type: 'input', readonly: 1 },
|
||||
{ name: 'match', display: 'Match all of the following', tag: 'postmaster_match' },
|
||||
{ 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: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
|
||||
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
@url: @apiPath + '/roles'
|
||||
@configure_attributes = [
|
||||
|
|
|
@ -53,8 +53,14 @@ class App.User extends App.Model
|
|||
cssClass += ' ' if cssClass
|
||||
cssClass += "size-#{ size }"
|
||||
|
||||
if @active is false
|
||||
cssClass += ' avatar--inactive'
|
||||
|
||||
if @isOutOfOffice()
|
||||
cssClass += ' avatar--vacation'
|
||||
|
||||
if placement
|
||||
placement = " data-placement='#{ placement }'"
|
||||
placement = " data-placement='#{placement}'"
|
||||
|
||||
if !avatar
|
||||
if type is 'personal'
|
||||
|
@ -104,6 +110,19 @@ class App.User extends App.Model
|
|||
vip: vip
|
||||
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: ->
|
||||
return if !@image
|
||||
# set image url
|
||||
|
@ -237,3 +256,16 @@ class App.User extends App.Model
|
|||
break
|
||||
return access if access
|
||||
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()
|
||||
|
|
|
@ -142,7 +142,7 @@
|
|||
<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>
|
||||
|
||||
<pre><code class="language-html js-paramsBlock"><script src="<%= @baseurl %>/assets/chat/chat.min.js"></script>
|
||||
<pre><code class="language-html js-code"><script src="<%= @baseurl %>/assets/chat/chat.min.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
new ZammadChat({
|
||||
|
@ -153,7 +153,7 @@ $(function() {
|
|||
|
||||
<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>
|
||||
<pre><code class="language-html js-paramsBlock"><button class="open-zammad-chat">Chat with us</button>
|
||||
<pre><code class="language-html js-code"><button class="open-zammad-chat">Chat with us</button>
|
||||
|
||||
<script src="<%= @baseurl %>/assets/chat/chat.min.js"></script>
|
||||
<script>
|
||||
|
|
|
@ -10,8 +10,20 @@
|
|||
<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>
|
||||
|
||||
<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>
|
||||
<form class="js-params">
|
||||
<form class="js-paramsDesigner">
|
||||
|
||||
<fieldset>
|
||||
<div class="input form-group formGroup--halfSize">
|
||||
|
@ -114,6 +126,9 @@
|
|||
</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"><script src="https://code.jquery.com/jquery-2.1.4.min.js"></script></code></pre>
|
||||
|
||||
<p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p>
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
<div class="chat-body js-body"></div>
|
||||
</div>
|
||||
<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>
|
|
@ -9,7 +9,7 @@
|
|||
<label for="application_id">Facebook APP ID <span>*</span></label>
|
||||
</div>
|
||||
<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 class="input form-group">
|
||||
|
@ -17,7 +17,7 @@
|
|||
<label for="application_secret">Facebook App Secret <span>*</span></label>
|
||||
</div>
|
||||
<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>
|
||||
<h2><%- @T('Your callback URL') %></h2>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<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 %>"/>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<input type="checkbox" value="<%= row.value %>" name="<%= @attribute.name %>" <%= row.checked %>/>
|
||||
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||
<%- @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>
|
||||
<% end %>
|
||||
</div>
|
|
@ -3,7 +3,7 @@
|
|||
<% if @attribute.multiple: %>
|
||||
<%- @tokens %>
|
||||
<% 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 %>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,6 +15,36 @@
|
|||
<%- @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>
|
||||
</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>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="<%= @attribute.class %>">
|
||||
<% for row in @attribute.options: %>
|
||||
<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-checked') %>
|
||||
</label>
|
||||
|
|
|
@ -1,25 +1,29 @@
|
|||
<div class="dropdown-toggle" data-toggle="dropdown">
|
||||
<input
|
||||
class="searchableSelect-shadow form-control js-shadow"
|
||||
id="<%= @id %>"
|
||||
name="<%= @name %>"
|
||||
<%= @required %>
|
||||
<%= @autofocus %>
|
||||
value="<%= @value %>"
|
||||
id="<%= @attribute.id %>"
|
||||
name="<%= @attribute.name %>"
|
||||
<%= @attribute.required %>
|
||||
<%= @attribute.autofocus %>
|
||||
value="<%= @attribute.value %>"
|
||||
>
|
||||
<input
|
||||
class="searchableSelect-main form-control js-input<%= " #{ @class }" if @class %>"
|
||||
placeholder="<%= @placeholder %>"
|
||||
value="<%= @valueName %>"
|
||||
autocomplete="new-password"
|
||||
class="searchableSelect-main form-control js-input<%= " #{ @attribute.class }" if @attribute.class %>"
|
||||
placeholder="<%= @attribute.placeholder %>"
|
||||
value="<%= @attribute.valueName %>"
|
||||
autocomplete="off"
|
||||
<%= @attribute.required %>
|
||||
>
|
||||
<div class="searchableSelect-autocomplete">
|
||||
<span class="searchableSelect-autocomplete-invisible js-autocomplete-invisible"></span>
|
||||
<span class="searchableSelect-autocomplete-visible js-autocomplete-visible"></span>
|
||||
</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>
|
||||
<ul class="dropdown-menu dropdown-menu-left js-optionsList" role="menu">
|
||||
<%- @renderedOptions %>
|
||||
</ul>
|
||||
<div class="dropdown-menu dropdown-menu-left dropdown-menu--has-submenu js-dropdown">
|
||||
<ul class="js-optionsList" role="menu">
|
||||
<%- @options %>
|
||||
</ul>
|
||||
<%- @submenus %>
|
||||
</div>
|
|
@ -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>
|
|
@ -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
Loading…
Reference in a new issue