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

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

View file

@ -3,7 +3,7 @@ before_script:
- which ruby
- 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

View file

@ -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:

View file

@ -1 +1 @@
2.3.1
2.4.1

View file

@ -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

View file

@ -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
View file

@ -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'

View file

@ -1,67 +1,82 @@
GIT
remote: https://github.com/thorsteneckel/autodiscover.git
revision: 29d713ee0c8c25fcf74c4292ff13fe1fa4d0d827
specs:
autodiscover (1.0.2)
httpclient
logging
nokogiri
nori
GIT
remote: https://github.com/wimm/rubyntlm.git
revision: 53969639b87b9e5d5fef560f19cf0d977259591c
specs:
rubyntlm (0.1.2)
GEM
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

View file

@ -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)

View file

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

View file

@ -61,13 +61,13 @@ class App.Controller extends Spine.Controller
clearDelay: (delay_id) =>
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)

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -45,9 +45,7 @@ class App.ChannelEmailFilter extends App.Controller
template = $( '<div><div class="overview"></div><a data-type="new" class="btn btn--success">' + App.i18n.translateContent('New') + '</a></div>' )
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 },
]

View file

@ -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, '&lt;')
.replace(/\>/g, '&gt;')
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')

View file

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

View file

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

View file

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

View file

@ -28,13 +28,20 @@ class Index extends App.ControllerIntegrationBase
super
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) =>

View file

@ -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'
})

View file

@ -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) =>

View file

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

View file

@ -2,7 +2,7 @@ class App.SettingsAreaProxy extends App.Controller
events:
'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(

View file

@ -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) ->

View file

@ -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
@ -37,4 +52,4 @@ class App.UiElement.permission extends App.UiElement.ApplicationUiElement
)
item
item

View file

@ -20,7 +20,7 @@ class App.UiElement.postmaster_set
name: 'Customer'
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:

View file

@ -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()

View file

@ -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]

View file

@ -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()

View file

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

View file

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

View file

@ -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')

View file

@ -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')

View file

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

View file

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

View file

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

View file

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

View file

@ -98,9 +98,13 @@ class App.TicketMerge extends App.ControllerModal
type: 'error'
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)
)

View file

@ -462,6 +462,14 @@ class ChatWindow extends App.Controller
)
)
# show text module UI
new App.WidgetTextModule(
el: @input
data:
user: App.Session.get()
config: App.Config.all()
)
focus: =>
@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 })

View file

@ -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')

View file

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

View file

@ -10,6 +10,7 @@ class Index extends App.ControllerContent
'.zendesk-api-token-error': 'apiTokenErrorMessage'
'#zendesk-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)

View file

@ -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() )

View file

@ -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'

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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'

View file

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

View file

@ -9,18 +9,32 @@ class App.TicketZoomSidebar extends App.ObserverController
if backend && backend.reload
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

View file

@ -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

View file

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

View file

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

View file

@ -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)

View file

@ -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) =>

View file

@ -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

View file

@ -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;')

View file

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

View file

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

View file

@ -86,16 +86,16 @@ class App.WidgetTag extends App.Controller
return
@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)

View file

@ -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()

View file

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

View file

@ -5,9 +5,9 @@ class App._CollectionSingletonBase
constructor: ->
@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)

View file

@ -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) ->

View file

@ -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()

View file

@ -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) =>

View file

@ -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

View file

@ -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(

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()
}

View file

@ -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 = "&nbsp;"
if (document.selection) { // IE
var range = document.selection.createRange()
range.pasteHTML(string)
}
else {
document.execCommand('insertHTML', false, string)
}
this.paste('&nbsp;')
}
}
}
@ -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)

View file

@ -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 () {

View file

@ -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

View file

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

View file

@ -6,7 +6,7 @@ class App.Job extends App.Model
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: '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 },

View file

@ -8,6 +8,7 @@ class App.Overview extends App.Model
{ name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, null: false, relation: 'Role', translate: true },
{ name: '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}"

View file

@ -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 },

View file

@ -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 = [

View file

@ -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()

View file

@ -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">&lt;script src="<%= @baseurl %>/assets/chat/chat.min.js"&gt;&lt;/script&gt;
<pre><code class="language-html js-code">&lt;script src="<%= @baseurl %>/assets/chat/chat.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
$(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">&lt;button class="open-zammad-chat"&gt;Chat with us&lt;/button&gt;
<pre><code class="language-html js-code">&lt;button class="open-zammad-chat"&gt;Chat with us&lt;/button&gt;
&lt;script src="<%= @baseurl %>/assets/chat/chat.min.js"&gt;&lt;/script&gt;
&lt;script&gt;

View file

@ -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">&lt;script src="https://code.jquery.com/jquery-2.1.4.min.js"&gt;&lt;/script&gt;</code></pre>
<p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p>

View file

@ -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>

View file

@ -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>

View file

@ -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 %>"/>

View file

@ -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>

View file

@ -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>

View file

@ -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 %>

View file

@ -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>

View file

@ -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>

View file

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

View file

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

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