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

This commit is contained in:
Thorsten Eckel 2017-09-07 14:07:40 +02:00
commit 3a2a34e064
369 changed files with 13601 additions and 1831 deletions

View file

@ -127,6 +127,17 @@ test:integration:email_deliver:
- ruby -I test/ test/integration/email_deliver_test.rb - ruby -I test/ test/integration/email_deliver_test.rb
- rake db:drop - rake db:drop
test:integration:email_keep_on_server:
stage: test
tags:
- core
script:
- export RAILS_ENV=test
- rake db:create
- rake db:migrate
- ruby -I test/ test/integration/email_keep_on_server_test.rb
- rake db:drop
test:integration:twitter: test:integration:twitter:
stage: test stage: test
tags: tags:
@ -332,7 +343,7 @@ test:integration:otrs_5_mysql:
- mysql - mysql
script: script:
- export RAILS_ENV=test - export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz599.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"
- rake db:create - rake db:create
- rake db:migrate - rake db:migrate
- ruby -I test/ test/integration/otrs_import_test.rb - ruby -I test/ test/integration/otrs_import_test.rb
@ -345,7 +356,7 @@ test:integration:otrs_5_postgresql:
- postgresql - postgresql
script: script:
- export RAILS_ENV=test - export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz599.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" - export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"
- rake db:create - rake db:create
- rake db:migrate - rake db:migrate
- ruby -I test/ test/integration/otrs_import_test.rb - ruby -I test/ test/integration/otrs_import_test.rb

View file

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

View file

@ -1 +1 @@
2.3.1 2.4.1

View file

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

View file

@ -1,8 +1,8 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '2.3.1' ruby '2.4.1'
gem 'rails', '4.2.8' gem 'rails', '4.2.9'
gem 'rails-observers' gem 'rails-observers'
gem 'activerecord-session_store' gem 'activerecord-session_store'
@ -40,6 +40,7 @@ gem 'omniauth-gitlab'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
gem 'omniauth-linkedin-oauth2' gem 'omniauth-linkedin-oauth2'
gem 'omniauth-twitter' gem 'omniauth-twitter'
gem 'omniauth-microsoft-office365'
gem 'twitter' gem 'twitter'
gem 'telegramAPI' gem 'telegramAPI'
@ -73,12 +74,16 @@ gem 'argon2'
gem 'writeexcel' gem 'writeexcel'
gem 'icalendar' gem 'icalendar'
gem 'icalendar-recurrence'
gem 'browser' gem 'browser'
# integrations # integrations
gem 'slack-notifier' gem 'slack-notifier'
gem 'clearbit' gem 'clearbit'
gem 'zendesk_api' gem 'zendesk_api'
gem 'viewpoint'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git'
# event machine # event machine
gem 'eventmachine' gem 'eventmachine'

View file

@ -1,44 +1,60 @@
GIT
remote: https://github.com/thorsteneckel/autodiscover.git
revision: 29d713ee0c8c25fcf74c4292ff13fe1fa4d0d827
specs:
autodiscover (1.0.2)
httpclient
logging
nokogiri
nori
GIT
remote: https://github.com/wimm/rubyntlm.git
revision: 53969639b87b9e5d5fef560f19cf0d977259591c
specs:
rubyntlm (0.1.2)
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actionmailer (4.2.8) actionmailer (4.2.9)
actionpack (= 4.2.8) actionpack (= 4.2.9)
actionview (= 4.2.8) actionview (= 4.2.9)
activejob (= 4.2.8) activejob (= 4.2.9)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.8) actionpack (4.2.9)
actionview (= 4.2.8) actionview (= 4.2.9)
activesupport (= 4.2.8) activesupport (= 4.2.9)
rack (~> 1.6) rack (~> 1.6)
rack-test (~> 0.6.2) rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2) rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.8) actionview (4.2.9)
activesupport (= 4.2.8) activesupport (= 4.2.9)
builder (~> 3.1) builder (~> 3.1)
erubis (~> 2.7.0) erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5) rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.3) rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (4.2.8) activejob (4.2.9)
activesupport (= 4.2.8) activesupport (= 4.2.9)
globalid (>= 0.3.0) globalid (>= 0.3.0)
activemodel (4.2.8) activemodel (4.2.9)
activesupport (= 4.2.8) activesupport (= 4.2.9)
builder (~> 3.1) builder (~> 3.1)
activerecord (4.2.8) activerecord (4.2.9)
activemodel (= 4.2.8) activemodel (= 4.2.9)
activesupport (= 4.2.8) activesupport (= 4.2.9)
arel (~> 6.0) arel (~> 6.0)
activerecord-nulldb-adapter (0.3.6) activerecord-nulldb-adapter (0.3.7)
activerecord (>= 2.0.0) activerecord (>= 2.0.0)
activerecord-session_store (1.0.0) activerecord-session_store (1.1.0)
actionpack (>= 4.0, < 5.1) actionpack (>= 4.0, < 5.2)
activerecord (>= 4.0, < 5.1) activerecord (>= 4.0, < 5.2)
multi_json (~> 1.11, >= 1.11.2) multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3) rack (>= 1.5.2, < 3)
railties (>= 4.0, < 5.1) railties (>= 4.0, < 5.2)
activesupport (4.2.8) activesupport (4.2.9)
i18n (~> 0.7) i18n (~> 0.7)
minitest (~> 5.1) minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4) thread_safe (~> 0.3, >= 0.3.4)
@ -49,9 +65,9 @@ GEM
ffi (~> 1.9) ffi (~> 1.9)
ffi-compiler (~> 0.1) ffi-compiler (~> 0.1)
ast (2.3.0) ast (2.3.0)
autoprefixer-rails (6.4.1.1) autoprefixer-rails (7.1.2.4)
execjs execjs
biz (1.6.0) biz (1.7.0)
clavius (~> 1.0) clavius (~> 1.0)
tzinfo tzinfo
browser (2.2.0) browser (2.2.0)
@ -60,7 +76,7 @@ GEM
childprocess (0.5.9) childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
clavius (1.0.2) clavius (1.0.2)
clearbit (0.2.5) clearbit (0.2.7)
nestful (~> 1.1.0) nestful (~> 1.1.0)
coderay (1.1.1) coderay (1.1.1)
coffee-rails (4.2.1) coffee-rails (4.2.1)
@ -69,27 +85,27 @@ GEM
coffee-script (2.4.1) coffee-script (2.4.1)
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.10.0) coffee-script-source (1.12.2)
coffeelint (1.14.0) coffeelint (1.14.0)
coffee-script coffee-script
execjs execjs
json json
composite_primary_keys (8.1.5) composite_primary_keys (8.1.6)
activerecord (~> 4.2.0) activerecord (~> 4.2.0)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
coveralls (0.8.16) coveralls (0.8.21)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov (~> 0.12.0) simplecov (~> 0.14.1)
term-ansicolor (~> 1.3.0) term-ansicolor (~> 1.3)
thor (~> 0.19.1) thor (~> 0.19.4)
tins (>= 1.6.0, < 2) tins (~> 1.6)
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) safe_yaml (~> 1.0.0)
daemons (1.2.4) daemons (1.2.4)
delayed_job (4.1.2) delayed_job (4.1.3)
activesupport (>= 3.0, < 5.1) activesupport (>= 3.0, < 5.2)
delayed_job_active_record (4.1.1) delayed_job_active_record (4.1.2)
activerecord (>= 3.0, < 5.1) activerecord (>= 3.0, < 5.2)
delayed_job (>= 3.0, < 5) delayed_job (>= 3.0, < 5)
diff-lcs (1.2.5) diff-lcs (1.2.5)
diffy (3.1.0) diffy (3.1.0)
@ -151,10 +167,11 @@ GEM
guard (~> 2.8) guard (~> 2.8)
guard-compat (~> 1.0) guard-compat (~> 1.0)
multi_json (~> 1.8) multi_json (~> 1.8)
guard-symlink (0.1.0) guard-symlink (0.1.1)
guard
guard-compat (~> 1.1) guard-compat (~> 1.1)
hashdiff (0.3.2) hashdiff (0.3.5)
hashie (3.4.4) hashie (3.5.6)
htmlentities (4.3.4) htmlentities (4.3.4)
http (1.0.4) http (1.0.4)
addressable (~> 2.3) addressable (~> 2.3)
@ -165,21 +182,30 @@ GEM
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (1.0.3) http-form_data (1.0.3)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
i18n (0.8.4) httpclient (2.8.3)
i18n (0.8.6)
icalendar (2.4.1) icalendar (2.4.1)
icalendar-recurrence (1.1.2)
icalendar (~> 2.0)
ice_cube (~> 0.13)
ice_cube (0.16.2)
inflection (1.0.0) inflection (1.0.0)
json (1.8.6) json (1.8.6)
jwt (1.5.4) jwt (1.5.6)
kgio (2.11.0) kgio (2.11.0)
koala (2.4.0) koala (2.4.0)
addressable addressable
faraday faraday
multi_json (>= 1.3.0) multi_json (>= 1.3.0)
libv8 (3.16.14.15) libv8 (3.16.14.19)
listen (3.1.5) listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2) ruby_dep (~> 1.2)
little-plugger (1.1.4)
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.0.3) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
lumberjack (1.0.10) lumberjack (1.0.10)
@ -192,9 +218,9 @@ GEM
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521) mime-types-data (3.2016.0521)
mini_portile2 (2.2.0) mini_portile2 (2.2.0)
minitest (5.10.2) minitest (5.10.3)
multi_json (1.12.1) multi_json (1.12.1)
multi_xml (0.5.5) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
mysql2 (0.4.6) mysql2 (0.4.6)
naught (1.1.0) naught (1.1.0)
@ -204,37 +230,41 @@ GEM
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.8.0) nokogiri (1.8.0)
mini_portile2 (~> 2.2.0) mini_portile2 (~> 2.2.0)
nori (2.6.0)
notiffany (0.1.1) notiffany (0.1.1)
nenv (~> 0.1) nenv (~> 0.1)
shellany (~> 0.0) shellany (~> 0.0)
oauth (0.5.1) oauth (0.5.1)
oauth2 (1.2.0) oauth2 (1.4.0)
faraday (>= 0.8, < 0.10) faraday (>= 0.8, < 0.13)
jwt (~> 1.0) jwt (~> 1.0)
multi_json (~> 1.3) multi_json (~> 1.3)
multi_xml (~> 0.5) multi_xml (~> 0.5)
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.4.1) octokit (4.4.1)
sawyer (~> 0.7.0, >= 0.5.3) sawyer (~> 0.7.0, >= 0.5.3)
omniauth (1.3.1) omniauth (1.6.1)
hashie (>= 1.2, < 4) hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.0, < 3) rack (>= 1.6.2, < 3)
omniauth-facebook (4.0.0) omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2) omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2) omniauth-github (1.3.0)
omniauth (~> 1.0) omniauth (~> 1.5)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (>= 1.4.0, < 2.0)
omniauth-gitlab (1.0.2) omniauth-gitlab (1.0.2)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0) omniauth-oauth2 (~> 1.0)
omniauth-google-oauth2 (0.4.1) omniauth-google-oauth2 (0.5.0)
jwt (~> 1.5.2) jwt (~> 1.5)
multi_json (~> 1.3) multi_json (~> 1.3)
omniauth (>= 1.1.1) omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.3.1) omniauth-oauth2 (>= 1.3.1)
omniauth-linkedin-oauth2 (0.1.5) omniauth-linkedin-oauth2 (0.1.5)
omniauth (~> 1.0) omniauth (~> 1.0)
omniauth-oauth2 omniauth-oauth2
omniauth-microsoft-office365 (0.0.7)
omniauth
omniauth-oauth2
omniauth-oauth (1.1.0) omniauth-oauth (1.1.0)
oauth oauth
omniauth (~> 1.0) omniauth (~> 1.0)
@ -246,32 +276,32 @@ GEM
omniauth-oauth (~> 1.1) omniauth-oauth (~> 1.1)
parser (2.3.1.2) parser (2.3.1.2)
ast (~> 2.2) ast (~> 2.2)
pg (0.18.4) pg (0.20.0)
pluginator (1.3.0) pluginator (1.5.0)
power_assert (0.3.1) power_assert (0.3.1)
powerpack (0.1.1) powerpack (0.1.1)
pre-commit (0.28.0) pre-commit (0.35.0)
pluginator (~> 1.1) pluginator (~> 1.5)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
slop (~> 3.4) slop (~> 3.4)
puma (3.6.0) puma (3.9.1)
rack (1.6.8) rack (1.6.8)
rack-livereload (0.3.16) rack-livereload (0.3.16)
rack rack
rack-test (0.6.3) rack-test (0.6.3)
rack (>= 1.0) rack (>= 1.0)
rails (4.2.8) rails (4.2.9)
actionmailer (= 4.2.8) actionmailer (= 4.2.9)
actionpack (= 4.2.8) actionpack (= 4.2.9)
actionview (= 4.2.8) actionview (= 4.2.9)
activejob (= 4.2.8) activejob (= 4.2.9)
activemodel (= 4.2.8) activemodel (= 4.2.9)
activerecord (= 4.2.8) activerecord (= 4.2.9)
activesupport (= 4.2.8) activesupport (= 4.2.9)
bundler (>= 1.3.0, < 2.0) bundler (>= 1.3.0, < 2.0)
railties (= 4.2.8) railties (= 4.2.9)
sprockets-rails sprockets-rails
rails-deprecated_sanitizer (1.0.3) rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha) activesupport (>= 4.2.0.alpha)
@ -281,15 +311,16 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1) rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3) rails-html-sanitizer (1.0.3)
loofah (~> 2.0) loofah (~> 2.0)
rails-observers (0.1.2) rails-observers (0.1.5)
activemodel (~> 4.0) activemodel (>= 4.0)
railties (4.2.8) railties (4.2.9)
actionpack (= 4.2.8) actionpack (= 4.2.9)
activesupport (= 4.2.8) activesupport (= 4.2.9)
rake (>= 0.8.7) rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.1.0) rainbow (2.2.2)
raindrops (0.17.0) rake
raindrops (0.19.0)
rake (12.0.0) rake (12.0.0)
rb-fsevent (0.9.7) rb-fsevent (0.9.7)
rb-inotify (0.9.7) rb-inotify (0.9.7)
@ -337,7 +368,6 @@ GEM
sawyer (0.7.0) sawyer (0.7.0)
addressable (>= 2.3.5, < 2.5) addressable (>= 2.3.5, < 2.5)
faraday (~> 0.8, < 0.10) faraday (~> 0.8, < 0.10)
scrub_rb (1.0.1)
selenium-webdriver (2.53.4) selenium-webdriver (2.53.4)
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.0) rubyzip (~> 1.0)
@ -345,16 +375,17 @@ GEM
shellany (0.0.1) shellany (0.0.1)
simple-rss (1.3.1) simple-rss (1.3.1)
simple_oauth (0.3.1) simple_oauth (0.3.1)
simplecov (0.12.0) simplecov (0.14.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
simplecov-html (0.10.0) simplecov-html (0.10.1)
simplecov-rcov (0.2.3) simplecov-rcov (0.2.3)
simplecov (>= 0.4.1) simplecov (>= 0.4.1)
slack-notifier (1.5.1) slack-notifier (1.5.1)
slop (3.6.0) slop (3.6.0)
spring (1.7.2) spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4) spring-commands-rspec (1.0.4)
spring (>= 0.9.1) spring (>= 0.9.1)
sprockets (3.7.1) sprockets (3.7.1)
@ -367,17 +398,17 @@ GEM
sqlite3 (1.3.11) sqlite3 (1.3.11)
telegramAPI (1.2.2) telegramAPI (1.2.2)
rest-client (~> 2.0, >= 1.7.3) rest-client (~> 2.0, >= 1.7.3)
term-ansicolor (1.3.2) term-ansicolor (1.6.0)
tins (~> 1.0) tins (~> 1.0)
test-unit (3.2.1) test-unit (3.2.1)
power_assert power_assert
therubyracer (0.12.2) therubyracer (0.12.3)
libv8 (~> 3.16.14.0) libv8 (~> 3.16.14.15)
ref ref
thor (0.19.4) thor (0.19.4)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.5) tilt (2.0.5)
tins (1.13.0) tins (1.15.0)
twitter (5.17.0) twitter (5.17.0)
addressable (~> 2.3) addressable (~> 2.3)
buftok (~> 0.2.0) buftok (~> 0.2.0)
@ -397,25 +428,29 @@ GEM
unf_ext unf_ext
unf_ext (0.0.7.4) unf_ext (0.0.7.4)
unicode-display_width (1.1.1) unicode-display_width (1.1.1)
unicorn (5.2.0) unicorn (5.3.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
valid_email2 (1.2.17) valid_email2 (2.0.0)
activemodel (>= 3.2) activemodel (>= 3.2)
mail (~> 2.5) mail (~> 2.5)
webmock (2.3.2) viewpoint (1.1.0)
httpclient
logging
nokogiri
rubyntlm
webmock (3.0.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
websocket (1.2.3) websocket (1.2.3)
writeexcel (1.0.5) writeexcel (1.0.5)
zendesk_api (1.14.0) zendesk_api (1.14.4)
faraday (~> 0.9) faraday (~> 0.9)
hashie (>= 1.2, < 4.0, != 3.3.0) hashie (>= 3.5.2, < 4.0.0)
inflection inflection
mime-types mime-types
multipart-post (~> 2.0) multipart-post (~> 2.0)
scrub_rb (~> 1.0.1)
PLATFORMS PLATFORMS
ruby ruby
@ -424,6 +459,7 @@ DEPENDENCIES
activerecord-nulldb-adapter activerecord-nulldb-adapter
activerecord-session_store activerecord-session_store
argon2 argon2
autodiscover!
autoprefixer-rails autoprefixer-rails
biz biz
browser browser
@ -449,6 +485,7 @@ DEPENDENCIES
guard-symlink guard-symlink
htmlentities htmlentities
icalendar icalendar
icalendar-recurrence
json json
koala koala
libv8 libv8
@ -463,17 +500,19 @@ DEPENDENCIES
omniauth-gitlab omniauth-gitlab
omniauth-google-oauth2 omniauth-google-oauth2
omniauth-linkedin-oauth2 omniauth-linkedin-oauth2
omniauth-microsoft-office365
omniauth-oauth2 omniauth-oauth2
omniauth-twitter omniauth-twitter
pg pg
pre-commit pre-commit
puma puma
rack-livereload rack-livereload
rails (= 4.2.8) rails (= 4.2.9)
rails-observers rails-observers
rb-fsevent rb-fsevent
rspec-rails rspec-rails
rubocop rubocop
rubyntlm!
sass-rails sass-rails
selenium-webdriver selenium-webdriver
simple-rss simple-rss
@ -491,12 +530,13 @@ DEPENDENCIES
uglifier uglifier
unicorn unicorn
valid_email2 valid_email2
viewpoint
webmock webmock
writeexcel writeexcel
zendesk_api zendesk_api
RUBY VERSION RUBY VERSION
ruby 2.3.1p112 ruby 2.4.1p111
BUNDLED WITH BUNDLED WITH
1.13.7 1.15.3

View file

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

View file

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

View file

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

View file

@ -108,7 +108,7 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
# show errors in form # show errors in form
if errors if errors
@log 'error', errors @log 'error', errors
@formValidate( form: e.target, errors: errors ) @formValidate(form: e.target, errors: errors)
return false return false
# disable form # disable form
@ -118,8 +118,10 @@ class App.ChannelEmailFilterEdit extends App.ControllerModal
object.save( object.save(
done: => done: =>
@close() @close()
fail: => fail: (settings, details) =>
@close() @log 'errors', details
@formEnable(e)
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
) )
class App.ChannelEmailSignature extends App.Controller class App.ChannelEmailSignature extends App.Controller
@ -201,7 +203,7 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
# show errors in form # show errors in form
if errors if errors
@log 'error', errors @log 'error', errors
@formValidate( form: e.target, errors: errors ) @formValidate(form: e.target, errors: errors)
return false return false
# disable form # disable form
@ -211,8 +213,10 @@ class App.ChannelEmailSignatureEdit extends App.ControllerModal
object.save( object.save(
done: => done: =>
@close() @close()
fail: => fail: (settings, details) =>
@log 'errors', details
@formEnable(e) @formEnable(e)
@form.showAlert(details.error_human || details.error || 'Unable to create object!')
) )
class App.ChannelEmailAccountOverview extends App.Controller class App.ChannelEmailAccountOverview extends App.Controller
@ -560,21 +564,24 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# inbound # inbound
configureAttributesInbound = [ configureAttributesInbound = [
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound }, { name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false }, { name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', }, { name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off' },
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true }, { name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' }, { name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' }, { name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false }, { name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' },
{ name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
] ]
showHideFolder = (params, attribute, attributes, classname, form, ui) -> showHideFolder = (params, attribute, attributes, classname, form, ui) ->
return if !params return if !params
if params.adapter is 'imap' if params.adapter is 'imap'
ui.show('options::folder') ui.show('options::folder')
ui.show('options::keep_on_server')
return return
ui.hide('options::folder') ui.hide('options::folder')
ui.hide('options::keep_on_server')
handlePort = (params, attribute, attributes, classname, form, ui) -> handlePort = (params, attribute, attributes, classname, form, ui) ->
return if !params return if !params
@ -606,9 +613,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# fill user / password based on intro info # fill user / password based on intro info
channel_used = { options: {} } channel_used = { options: {} }
if @account['meta'] if @account['meta']
channel_used['options']['user'] = @account['meta']['email'] channel_used['options']['user'] = @account['meta']['email']
channel_used['options']['password'] = @account['meta']['password'] channel_used['options']['password'] = @account['meta']['password']
channel_used['options']['folder'] = @account['meta']['folder'] channel_used['options']['folder'] = @account['meta']['folder']
channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server']
# show used backend # show used backend
@$('.base-outbound-settings').html('') @$('.base-outbound-settings').html('')
@ -670,7 +678,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
for key, value of data.setting for key, value of data.setting
@account[key] = value @account[key] = value
if data.content_messages && data.content_messages > 0 if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
@$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-message').html(message)
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro') @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
@ -724,7 +732,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# remember account settings # remember account settings
@account.inbound = params @account.inbound = params
if data.content_messages && data.content_messages > 0 if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages) message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
@$('.js-inbound-acknowledge .js-message').html(message) @$('.js-inbound-acknowledge .js-message').html(message)
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound') @$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')

View file

@ -9,7 +9,7 @@ class App.ChannelForm extends App.ControllerSubContent
'change .js-paramsSetting select': 'updateGroup' 'change .js-paramsSetting select': 'updateGroup'
elements: elements:
'.js-paramsBlock': 'paramsBlock' '.js-code': 'code'
'.js-paramsSetting': 'paramsSetting' '.js-paramsSetting': 'paramsSetting'
'.js-formSetting input': 'formSetting' '.js-formSetting input': 'formSetting'
@ -43,7 +43,7 @@ class App.ChannelForm extends App.ControllerSubContent
@html element @html element
@paramsBlock.each (i, block) -> @code.each (i, block) ->
hljs.highlightBlock block hljs.highlightBlock block
@updateParamsDesigner() @updateParamsDesigner()

View file

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

View file

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

View file

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

View file

@ -28,13 +28,20 @@ class Index extends App.ControllerIntegrationBase
super super
active = @$('.js-switch input').prop('checked') active = @$('.js-switch input').prop('checked')
if active if active
@ajax( job_start = =>
id: 'jobs_config' @ajax(
type: 'POST' id: 'jobs_config'
url: "#{@apiPath}/integration/ldap/job_start" type: 'POST'
processData: true url: "#{@apiPath}/integration/ldap/job_start"
success: (data, status, xhr) => processData: true
@render(true) success: (data, status, xhr) =>
@render(true)
)
App.Delay.set(
job_start,
600,
'job_start',
) )
class Form extends App.Controller class Form extends App.Controller
@ -91,6 +98,7 @@ class Form extends App.Controller
processData: true processData: true
success: (data, status, xhr) => success: (data, status, xhr) =>
@render(true) @render(true)
@lastResult()
) )
startWizard: (e) => startWizard: (e) =>

View file

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

View file

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

View file

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

View file

@ -82,7 +82,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
view: view:
shown: true shown: true
invite_customer: invite_customer:
show: false shown: false
required: false required: false
'admin.user': 'admin.user':
create: create:
@ -94,10 +94,10 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
view: view:
shown: true shown: true
invite_agent: invite_agent:
show: false shown: false
required: false required: false
invite_customer: invite_customer:
show: false shown: false
required: false required: false
Organization: Organization:
'ticket.customer': 'ticket.customer':

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -391,6 +391,7 @@ class App.TicketZoomArticleActions extends App.Controller
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || '' body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
# check if quote need to be added # check if quote need to be added
signaturePosition = 'bottom'
selected = App.ClipBoard.getSelected('html') selected = App.ClipBoard.getSelected('html')
if selected if selected
selected = App.Utils.htmlCleanup(selected).html() selected = App.Utils.htmlCleanup(selected).html()
@ -399,6 +400,16 @@ class App.TicketZoomArticleActions extends App.Controller
if selected if selected
selected = App.Utils.textCleanup(selected) selected = App.Utils.textCleanup(selected)
selected = App.Utils.text2html(selected) selected = App.Utils.text2html(selected)
# 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)
if selected if selected
selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>" selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>"
@ -409,7 +420,12 @@ class App.TicketZoomArticleActions extends App.Controller
type = App.TicketArticleType.findByAttribute(name:'email') type = App.TicketArticleType.findByAttribute(name:'email')
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } ) App.Event.trigger('ui::ticket::setArticleType', {
ticket: @ticket
type: type
article: articleNew
signaturePosition: signaturePosition
})
telegramPersonalMessageReply: (e) => telegramPersonalMessageReply: (e) =>
e.preventDefault() e.preventDefault()

View file

@ -28,7 +28,62 @@ class App.TicketZoomArticleNew extends App.Controller
constructor: -> constructor: ->
super super
# set possble article types @internalSelector = true
@type = @defaults['type'] || 'note'
@setPossibleArticleTypes()
if @permissionCheck('ticket.customer')
@internalSelector = false
@textareaHeight =
open: 148
closed: 20
@dragEventCounter = 0
@attachments = []
@render()
if @defaults.body or @isIE10()
@openTextarea(null, true)
# set article type and expand text area
@bind('ui::ticket::setArticleType', (data) =>
return if data.ticket.id.toString() isnt @ticket_id.toString()
@openTextarea(null, true)
for key, value of data.article
if key is 'body'
@$('[data-name="' + key + '"]').html(value)
else
@$('[name="' + key + '"]').val(value).trigger('change')
# preselect article type
@setArticleType(data.type.name, data.signaturePosition)
# set focus at end of field
if data.position is 'end'
@placeCaretAtEnd(@textarea.get(0))
return
# set focus into field
@textarea.focus()
)
# reset new article screen
@bind('ui::ticket::taskReset', (data) =>
return if data.ticket_id.toString() isnt @ticket_id.toString()
@type = 'note'
@defaults = {}
@render()
)
# rerender, e. g. on language change
@bind('ui:rerender', =>
@render()
)
setPossibleArticleTypes: =>
possibleArticleType = possibleArticleType =
note: true note: true
phone: true phone: true
@ -50,12 +105,9 @@ class App.TicketZoomArticleNew extends App.Controller
possibleArticleType['email'] = true possibleArticleType['email'] = true
# gets referenced in @setArticleType # gets referenced in @setArticleType
@internalSelector = true
@type = @defaults['type'] || 'note'
@articleTypes = [] @articleTypes = []
if possibleArticleType.note if possibleArticleType.note
internal = @Config.get('ui_ticket_zoom_article_new_internal') internal = @Config.get('ui_ticket_zoom_article_note_new_internal')
@articleTypes.push { @articleTypes.push {
name: 'note' name: 'note'
icon: 'note' icon: 'note'
@ -64,10 +116,13 @@ class App.TicketZoomArticleNew extends App.Controller
features: ['attachment'] features: ['attachment']
} }
if possibleArticleType.email if possibleArticleType.email
attributes = ['to', 'cc', 'subject']
if !@Config.get('ui_ticket_zoom_article_email_subject')
attributes = ['to', 'cc']
@articleTypes.push { @articleTypes.push {
name: 'email' name: 'email'
icon: 'email' icon: 'email'
attributes: ['to', 'cc'] attributes: attributes
internal: false, internal: false,
features: ['attachment'] features: ['attachment']
} }
@ -80,6 +135,9 @@ class App.TicketZoomArticleNew extends App.Controller
features: [] features: []
} }
if possibleArticleType['twitter status'] if possibleArticleType['twitter status']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push { @articleTypes.push {
name: 'twitter status' name: 'twitter status'
icon: 'twitter' icon: 'twitter'
@ -90,6 +148,9 @@ class App.TicketZoomArticleNew extends App.Controller
warningTextLength: 30 warningTextLength: 30
} }
if possibleArticleType['twitter direct-message'] if possibleArticleType['twitter direct-message']
attributes = ['body:limit', 'body:initials']
if !@Config.get('ui_ticket_zoom_article_twitter_initials')
attributes = ['body:limit']
@articleTypes.push { @articleTypes.push {
name: 'twitter direct-message' name: 'twitter direct-message'
icon: 'twitter' icon: 'twitter'
@ -130,57 +191,6 @@ class App.TicketZoomArticleNew extends App.Controller
}, },
] ]
if @permissionCheck('ticket.customer')
@internalSelector = false
@textareaHeight =
open: 148
closed: 20
@dragEventCounter = 0
@attachments = []
@render()
if @defaults.body or @isIE10()
@openTextarea(null, true)
# set article type and expand text area
@bind('ui::ticket::setArticleType', (data) =>
return if data.ticket.id.toString() isnt @ticket_id.toString()
@openTextarea(null, true)
for key, value of data.article
if key is 'body'
@$('[data-name="' + key + '"]').html(value)
else
@$('[name="' + key + '"]').val(value).trigger('change')
# preselect article type
@setArticleType(data.type.name)
# set focus at end of field
if data.position is 'end'
@placeCaretAtEnd(@textarea.get(0))
return
# set focus into field
@textarea.focus()
)
# reset new article screen
@bind('ui::ticket::taskReset', (data) =>
return if data.ticket_id.toString() isnt @ticket_id.toString()
@type = 'note'
@defaults = {}
@render()
)
# rerender, e. g. on language change
@bind('ui:rerender', =>
@render()
)
placeCaretAtEnd: (el) -> placeCaretAtEnd: (el) ->
el.focus() el.focus()
if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined' if typeof window.getSelection isnt 'undefined' && typeof document.createRange isnt 'undefined'
@ -300,6 +310,7 @@ class App.TicketZoomArticleNew extends App.Controller
data: data:
ticket: ticket ticket: ticket
user: App.Session.get() user: App.Session.get()
config: App.Config.all()
) )
callback = (ticket) -> callback = (ticket) ->
textModule.reload( textModule.reload(
@ -318,9 +329,6 @@ class App.TicketZoomArticleNew extends App.Controller
params.form_id = @form_id params.form_id = @form_id
params.content_type = 'text/html' params.content_type = 'text/html'
if !params['internal']
params['internal'] = false
if @permissionCheck('ticket.customer') if @permissionCheck('ticket.customer')
sender = App.TicketArticleSender.findByAttribute('name', 'Customer') sender = App.TicketArticleSender.findByAttribute('name', 'Customer')
type = App.TicketArticleType.findByAttribute('name', 'web') type = App.TicketArticleType.findByAttribute('name', 'web')
@ -332,6 +340,11 @@ class App.TicketZoomArticleNew extends App.Controller
params.sender_id = sender.id params.sender_id = sender.id
params.type_id = type.id params.type_id = type.id
if params.internal
params.internal = true
else
params.internal = false
if params.type is 'twitter status' if params.type is 'twitter status'
App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false) App.Utils.htmlRemoveRichtext(@$('[data-name=body]'), false)
params.content_type = 'text/plain' params.content_type = 'text/plain'
@ -471,13 +484,15 @@ class App.TicketZoomArticleNew extends App.Controller
@$('[name=internal]').val('') @$('[name=internal]').val('')
setArticleType: (type) => setArticleType: (type, signaturePosition = 'bottom') =>
wasScrolledToBottom = @isScrolledToBottom() wasScrolledToBottom = @isScrolledToBottom()
@type = type @type = type
@$('[name=type]').val(type).trigger('change') @$('[name=type]').val(type).trigger('change')
@articleNewEdit.attr('data-type', type) @articleNewEdit.attr('data-type', type)
@$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide') @$('.js-selectableTypes').addClass('hide').filter("[data-type='#{type}']").removeClass('hide')
@setPossibleArticleTypes()
# get config # get config
config = {} config = {}
for articleTypeConfig in @articleTypes for articleTypeConfig in @articleTypes
@ -510,7 +525,7 @@ class App.TicketZoomArticleNew extends App.Controller
@$('[data-name=body] [data-signature="true"]').remove() @$('[data-name=body] [data-signature="true"]').remove()
# apply new signature # apply new signature
signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), ticket: ticketCurrent }) signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), ticket: ticketCurrent, config: App.Config.all() })
body = @$('[data-name=body]') body = @$('[data-name=body]')
if App.Utils.signatureCheck(body.html() || '', signatureFinished) if App.Utils.signatureCheck(body.html() || '', signatureFinished)
@ -518,7 +533,10 @@ class App.TicketZoomArticleNew extends App.Controller
body.append('<br><br>') body.append('<br><br>')
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>") signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
App.Utils.htmlStrip(signature) App.Utils.htmlStrip(signature)
body.append(signature) if signaturePosition is 'top'
body.prepend(signature)
else
body.append(signature)
@$('[data-name=body]').replaceWith(body) @$('[data-name=body]').replaceWith(body)
# remove old signature # remove old signature
@ -552,6 +570,20 @@ class App.TicketZoomArticleNew extends App.Controller
@delay(@updateLetterCount, 600) @delay(@updateLetterCount, 600)
@$('.js-textSizeLimit').removeClass('hide') @$('.js-textSizeLimit').removeClass('hide')
# convert remote src images to data uri
@$('[data-name=body] img').each( (i,image) ->
$image = $(image)
src = $image.attr('src')
if !_.isEmpty(src) && !src.match(/^data:image/i)
canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
ctx = canvas.getContext('2d')
ctx.drawImage(image, 0, 0)
dataURL = canvas.toDataURL()
$image.attr('src', dataURL)
)
@scrollToBottom() if wasScrolledToBottom @scrollToBottom() if wasScrolledToBottom
isScrolledToBottom: -> isScrolledToBottom: ->

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,11 @@ class App.i18n
_instance ?= new _i18nSingleton() _instance ?= new _i18nSingleton()
_instance.date(args, offset) _instance.date(args, offset)
@dir: ->
if _instance == undefined
_instance ?= new _i18nSingleton()
_instance.dir()
@get: -> @get: ->
if _instance == undefined if _instance == undefined
_instance ?= new _i18nSingleton() _instance ?= new _i18nSingleton()
@ -88,6 +93,10 @@ class _i18nSingleton extends Spine.Module
@_notTranslated = {} @_notTranslated = {}
@dateFormat = 'yyyy-mm-dd' @dateFormat = 'yyyy-mm-dd'
@timestampFormat = 'yyyy-mm-dd HH:MM' @timestampFormat = 'yyyy-mm-dd HH:MM'
@dirToSet = 'ltr'
dir: ->
@dirToSet
get: -> get: ->
@locale @locale
@ -96,7 +105,7 @@ class _i18nSingleton extends Spine.Module
# prepare locale # prepare locale
localeToSet = localeToSet.toLowerCase() localeToSet = localeToSet.toLowerCase()
dirToSet = 'ltr' @dirToSet = 'ltr'
# check if locale exists # check if locale exists
localeFound = false localeFound = false
@ -104,7 +113,7 @@ class _i18nSingleton extends Spine.Module
for locale in locales for locale in locales
if locale.locale is localeToSet if locale.locale is localeToSet
localeToSet = locale.locale localeToSet = locale.locale
dirToSet = locale.dir @dirToSet = locale.dir
localeFound = true localeFound = true
# try aliases # try aliases
@ -112,7 +121,7 @@ class _i18nSingleton extends Spine.Module
for locale in locales for locale in locales
if locale.alias is localeToSet if locale.alias is localeToSet
localeToSet = locale.locale localeToSet = locale.locale
dirToSet = locale.dir @dirToSet = locale.dir
localeFound = true localeFound = true
# if no locale and no alias was found, try to find correct one # if no locale and no alias was found, try to find correct one
@ -123,7 +132,7 @@ class _i18nSingleton extends Spine.Module
for locale in locales for locale in locales
if locale.alias is localeToSet if locale.alias is localeToSet
localeToSet = locale.locale localeToSet = locale.locale
dirToSet = locale.dir @dirToSet = locale.dir
localeFound = true localeFound = true
# check if locale need to be changed # check if locale need to be changed
@ -137,7 +146,7 @@ class _i18nSingleton extends Spine.Module
# set lang and dir attribute of html tag # set lang and dir attribute of html tag
$('html').prop('lang', localeToSet.substr(0, 2)) $('html').prop('lang', localeToSet.substr(0, 2))
$('html').prop('dir', dirToSet) $('html').prop('dir', @dirToSet)
@mapString = {} @mapString = {}
App.Ajax.request( App.Ajax.request(

View file

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

View file

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

View file

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

View file

@ -124,10 +124,12 @@ class App.Utils
@linkify: (string) -> @linkify: (string) ->
window.linkify(string) window.linkify(string)
# htmlEscapedAndLinkified = App.Utils.linkify(rawText) # htmlEscapedAndPhoneified = App.Utils.phoneify(rawText)
@phoneify: (string) -> @phoneify: (string) ->
string = string.replace(/\s+/g, '') return string if _.isEmpty(string)
"tel://#{encodeURIComponent(string)}" string = string.replace(/[^0-9,\+,#,\*]+/g, '')
.replace(/(.)\+/, '$1')
"tel:#{string}"
# wrappedText = App.Utils.wrap(rawText, maxLineLength) # wrappedText = App.Utils.wrap(rawText, maxLineLength)
@wrap: (ascii, max = 82) -> @wrap: (ascii, max = 82) ->
@ -649,6 +651,7 @@ class App.Utils
# textReplaced = App.Utils.replaceTags( template, { user: { firstname: 'Bob', lastname: 'Smith' } } ) # textReplaced = App.Utils.replaceTags( template, { user: { firstname: 'Bob', lastname: 'Smith' } } )
@replaceTags: (template, objects) -> @replaceTags: (template, objects) ->
template = template.replace( /#\{\s{0,2}(.+?)\s{0,2}\}/g, (index, key) -> template = template.replace( /#\{\s{0,2}(.+?)\s{0,2}\}/g, (index, key) ->
key = key.replace(/<.+?>/g, '')
levels = key.split(/\./) levels = key.split(/\./)
dataRef = objects dataRef = objects
for level in levels for level in levels

File diff suppressed because one or more lines are too long

View file

@ -88,7 +88,7 @@
// handle enter // handle enter
this.$element.on('keydown', function (e) { this.$element.on('keydown', function (e) {
_this.log('keydown', e.keyCode) _this.log('keydown', e.keyCode)
if ( _this.preventInput ) { if (_this.preventInput) {
this.log('preventInput', _this.preventInput) this.log('preventInput', _this.preventInput)
return return
} }
@ -97,18 +97,29 @@
if (e.keyCode === 13) { if (e.keyCode === 13) {
// disbale multi line // disbale multi line
if ( !_this.options.multiline ) { if (!_this.options.multiline) {
e.preventDefault() e.preventDefault()
return return
} }
// break <blockquote> after enter on empty line // break <blockquote> after enter on empty line
sel = window.getSelection() sel = window.getSelection()
node = $(sel.anchorNode) if (sel) {
if (node.parent().is('blockquote')) { node = $(sel.anchorNode)
if (node && node.parent() && node.parent().is('blockquote')) {
e.preventDefault()
document.execCommand('Insertparagraph')
document.execCommand('Outdent')
return
}
}
// behavior to enter new line on alt+enter
// on alt + enter not realy newline is fired, to make
// it compat. to other systems, do it here
if (!e.shiftKey && e.altKey && !e.ctrlKey && !e.metaKey) {
e.preventDefault() e.preventDefault()
document.execCommand('Insertparagraph') _this.paste('<br><br>')
document.execCommand('Outdent')
return return
} }
} }
@ -237,7 +248,7 @@
// limit check // limit check
if ( !_this.allowKey(e) ) { if ( !_this.allowKey(e) ) {
if ( !_this.maxLengthOk( 1 ) ) { if ( !_this.maxLengthOk(1) ) {
e.preventDefault() e.preventDefault()
return return
} }
@ -295,7 +306,7 @@
else { else {
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">" img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
} }
document.execCommand('insertHTML', false, img) _this.paste(img)
} }
// resize if to big // resize if to big
@ -367,13 +378,7 @@
text = App.Utils.removeEmptyLines(text) text = App.Utils.removeEmptyLines(text)
_this.log('insert', text) _this.log('insert', text)
// as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower) _this.paste(text)
if (docType == 'text3') {
_this.pasteHtmlAtCaret(text)
}
else {
document.execCommand('insertHTML', false, text)
}
return true return true
}) })
@ -533,37 +538,6 @@
return this.$element.html().trim() return this.$element.html().trim()
} }
// taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
Plugin.prototype.pasteHtmlAtCaret = function(html) {
var sel, range;
if (window.getSelection) {
sel = window.getSelection()
if (sel.getRangeAt && sel.rangeCount) {
range = sel.getRangeAt(0)
range.deleteContents()
var el = document.createElement('div')
el.innerHTML = html;
var frag = document.createDocumentFragment(), node, lastNode
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node)
}
range.insertNode(frag)
if (lastNode) {
range = range.cloneRange()
range.setStartAfter(lastNode)
range.collapse(true)
sel.removeAllRanges()
sel.addRange(range)
}
}
}
else if (document.selection && document.selection.type != 'Control') {
document.selection.createRange().pasteHTML(html)
}
}
// log method // log method
Plugin.prototype.log = function() { Plugin.prototype.log = function() {
if (App && App.Log) { if (App && App.Log) {
@ -574,7 +548,30 @@
} }
} }
$.fn[pluginName] = function ( options ) { // paste some content
Plugin.prototype.paste = function(string) {
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
// IE <= 10
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange()
if (range.pasteHTML) {
range.pasteHTML(string)
}
}
// IE == 11
else if (isIE11 && document.getSelection) {
var range = document.getSelection().getRangeAt(0)
var nnode = document.createElement('div')
range.surroundContents(nnode)
nnode.innerHTML = string
}
else {
document.execCommand('insertHTML', false, string)
}
}
$.fn[pluginName] = function (options) {
return this.each(function () { return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) { if (!$.data(this, 'plugin_' + pluginName)) {
$.data(this, 'plugin_' + pluginName, $.data(this, 'plugin_' + pluginName,
@ -586,6 +583,9 @@
// get correct val if textbox // get correct val if textbox
$.fn.ceg = function() { $.fn.ceg = function() {
var plugin = $.data(this[0], 'plugin_' + pluginName) var plugin = $.data(this[0], 'plugin_' + pluginName)
if (!plugin) {
return
}
return plugin.value() return plugin.value()
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -126,6 +126,9 @@
</div> </div>
</div> </div>
</div> </div>
<h3><%- @T('Requirements') %></h3>
<p><%- @T("Zammad Forms requires jQuery. If you don't already use it on your website include it like this:") %></p>
<pre><code class="language-html js-code">&lt;script src="https://code.jquery.com/jquery-2.1.4.min.js"&gt;&lt;/script&gt;</code></pre>
<p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p> <p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p>

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@
<div class="sidebar-content"></div> <div class="sidebar-content"></div>
</div> </div>
<% end %> <% end %>
<div class="tabsSidebar-tabs" style="margin-left: -<%- @scrollbarWidth %>px"> <div class="tabsSidebar-tabs" style="<%- if @dir is 'rtl' then 'margin-right' else 'margin-left' %>: -<%- @scrollbarWidth %>px">
<% for item in @items: %> <% for item in @items: %>
<div class="tabsSidebar-tab" data-tab="<%= item.name %>"> <div class="tabsSidebar-tab" data-tab="<%= item.name %>">
<%- @Icon(item.icon) %> <%- @Icon(item.icon) %>

View file

@ -18,7 +18,7 @@
<input type="checkbox" value="<%= role.id %>" name="role_ids" <% if @rolesSelected[role.id]: %>checked<% end %>/> <input type="checkbox" value="<%= role.id %>" name="role_ids" <% if @rolesSelected[role.id]: %>checked<% end %>/>
<%- @Icon('checkbox', 'icon-unchecked') %> <%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %> <%- @Icon('checkbox-checked', 'icon-checked') %>
<span class="label-text"><%= role.displayName() %> <% if role.note: %>- <span class="help-text"><%= role.note %></span><% end %></span> <span class="label-text"><%- @T(role.displayName()) %> <% if role.note: %>- <span class="help-text"><%- @T(role.note) %></span><% end %></span>
</label> </label>
<% if role.permissions: %> <% if role.permissions: %>
<% for permission in role.permissions: %> <% for permission in role.permissions: %>

View file

@ -63,7 +63,7 @@
</form> </form>
<form class="setup wizard hide js-inbound"> <form class="setup wizard hide js-inbound">
<div class="wizard-slide"> <div class="wizard-slide wizard-slide--large">
<h2><%- @T('Email Inbound') %></h2> <h2><%- @T('Email Inbound') %></h2>
<div class="wizard-body vertical justified"> <div class="wizard-body vertical justified">
<div class="alert alert--danger hide" role="alert"></div> <div class="alert alert--danger hide" role="alert"></div>

View file

@ -61,6 +61,7 @@
<div class="wizard-slide vertical hide" data-slide="zendesk-import"> <div class="wizard-slide vertical hide" data-slide="zendesk-import">
<h2><%- @T('%s Migration', 'Zendesk') %></h2> <h2><%- @T('%s Migration', 'Zendesk') %></h2>
<div class="alert alert--danger hide js-error" role="alert"></div> <div class="alert alert--danger hide js-error" role="alert"></div>
<div class="alert alert--info hide js-ticket-count-info" role="alert"><%- @T("There are more than 1000 tickets in the Zendesk system. Due to API rate limit restrictions we can't get the exact number of tickets yet and have to fetch them in batches of 1000. This might take some time, better grab a cup of coffee. The total number of tickets gets updated as soon as the currently known number is surpassed.") %></div>
<div class="wizard-body flex vertical justified"> <div class="wizard-body flex vertical justified">
<table class="progressTable"> <table class="progressTable">
<tr class="js-group"> <tr class="js-group">

View file

@ -10,9 +10,10 @@
<div class="page-content"> <div class="page-content">
<% if @description: %> <% if @description: %>
<% for item in @description: %> <% for item in @description: %>
<p><%- @T(item[0], item[1], item[2]) %></p> <p><%- @T(item...) %></p>
<% end %> <% end %>
<% end %> <% end %>
<div class="js-form"></div> <div class="js-form"></div>
<div class="js-scriptSnipped"></div>
<div class="js-log"></div> <div class="js-log"></div>
</div> </div>

View file

@ -0,0 +1,71 @@
<div class="js-lastImport"></div>
<div class="js-notConfigured">
<p><%- @T('No %s configured.', 'Exchange') %></p>
<button type="submit" class="btn btn--primary js-wizard"><%- @T('Configure') %></button>
</div>
<div class="js-summary hide">
<h2><%- @T('Settings') %></h2>
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="30%"><%- @T('Name') %>
<th width="70%"><%- @T('Value') %>
</thead>
<tbody>
<tr>
<td class="settings-list-row-control"><%- @T('Endpoint') %>
<td class="settings-list-row-control"><%= @config.endpoint %>
<tbody>
<tr>
<td class="settings-list-row-control"><%- @T('User') %>
<td class="settings-list-row-control"><%= @config.user %>
<tr>
<td class="settings-list-row-control"><%- @T('Password') %>
<td class="settings-list-row-control"><%= @M(@config.password) %>
</tbody>
</table>
<h2><%- @T('Mapping') %></h2>
<h3><%- @T('Folders') %></h3>
<% if _.isEmpty(@folders): %>
<table class="settings-list settings-list--stretch settings-list--placeholder">
<thead><tr><th><%- @T('No Entries') %>
</table>
<% else: %>
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th><%- @T('Folder') %>
<% for folder_name in @folders: %>
<tr>
<td class="settings-list-row-control"><%= folder_name %>
<% end %>
</thead>
<tbody>
</table>
<% end %>
<h3><%- @T('User') %></h3>
<% if _.isEmpty(@config.attributes): %>
<table class="settings-list settings-list--stretch settings-list--placeholder">
<thead><tr><th><%- @T('No Entries') %>
</table>
<% else: %>
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="40%"><%- @T('Exchange') %>
<th width="60%"><%- @T('Zammad') %>
<% for key, value of @config.attributes: %>
<tr>
<td class="settings-list-row-control"><%= key %>
<td class="settings-list-row-control"><%= value %>
<% end %>
</thead>
<tbody>
</table>
<% end %>
<button type="submit" class="btn btn--primary js-wizard"><%- @T('Change') %></button>
</div>

View file

@ -0,0 +1,54 @@
<div class="box box--message">
<h2><%- @T('Last sync') %></h2>
<% if _.isEmpty(@job.started_at): %>
<% if @job.result && @job.result.error: %>
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
<% else if @job.result && @job.result.info: %>
<div class="alert alert--info" role="alert"><%- @T('Info: %s', @job.result.info) %></div>
<% else: %>
<p><%- @T('Job is waiting to get started...') %></p>
<% end %>
<% else: %>
<% if @job.finished_at: %>
<p><%- @Ttimestamp(@job.started_at) %> - <%- @Ttimestamp(@job.finished_at) %></p>
<% if @job.result && @job.result.error: %>
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
<% else if @job.result && @job.result.info: %>
<div class="alert alert--info" role="alert"><%- @T('Info: %s', @job.result.info) %></div>
<% end %>
<% else: %>
<% if @job.result && @job.result.error: %>
<p><%- @Ttimestamp(@job.started_at) %></p>
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
<% else if !@countDone: %>
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Counting entries. This may take a while.') %></p>
<% else: %>
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p>
<div class="flex">
<progress max="<%= @job.result.sum %>" value="<%= @countDone %>"></progress>
</div>
<% end %>
<% end %>
<% if !_.isEmpty(@job.result) && @countDone: %>
<ul>
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
<ul>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
</ul>
</li>
<% if !_.isEmpty(@job.result.folders): %>
<li><%- @T('%s folders', 'Exchange') %>:
<ul>
<% for folder, result of @job.result.folders: %>
<li><%- folder %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>
<% end %>
</ul>
</li>
<% end %>
</ul>
<% end %>
<% if @job.finished_at: %>
<button type="submit" class="btn btn--primary js-start-sync"><%- @T('Start new') %></button>
<% end %>
<% end %>
</div>

View file

@ -0,0 +1,16 @@
<ul>
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>):
<ul>
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
</ul>
</li>
<% if !_.isEmpty(@job.result.folders): %>
<li><%- @T('%s folders', 'Exchange') %>:
<ul>
<% for folder, result of @job.result.folders: %>
<li><%- folder %>: <%= result.created %> <%- @T('created') %>, <%= result.updated %> <%- @T('updated') %>, <%= result.unchanged %> <%- @T('untouched') %>, <%= result.failed %> <%- @T('failed') %>
<% end %>
</ul>
</li>
<% end %>
</ul>

View file

@ -0,0 +1,7 @@
<tr class="js-entry">
<td style="max-width: 240px" class="settings-list-control-cell js-exchangeAttribute">
<td class="settings-list-control-cell js-userAttribute">
<td class="settings-list-row-control">
<div class="btn btn--text js-remove">
<%- @Icon('trash') %> <%- @T('Remove') %>
</div>

View file

@ -0,0 +1,258 @@
<div class="modal-dialog">
<form class="modal-content setup wizard js-discover">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<div class="alert alert--danger hide" role="alert"></div>
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="30%"><%- @T('Name') %>
<th width="70%"><%- @T('Value') %>
</thead>
<tbody>
<tr>
<td class="settings-list-row-control"><%- @T('User') %>
<td class="settings-list-control-cell"><input type="text" name="user" class="form-control form-control--small js-user" value="" placeholder="" autocomplete="off" required>
<tr>
<td class="settings-list-row-control"><%- @T('Password') %>
<td class="settings-list-control-cell"><input type="password" name="password" class="form-control form-control--small js-password" value="" placeholder="" autocomplete="new-password" required>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<div class="modal-rightFooter">
<button class="btn btn--primary align-right js-submit"><%- @T('Connect') %></button>
</div>
</div>
</form>
<form class="modal-content setup wizard hide js-connect">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<p class="wizard-loadingText">
<span class="loading icon"></span> <%- @T('Connecting ...') %>
</p>
</div>
</div>
<div class="modal-footer"></div>
</form>
<form class="modal-content setup wizard hide js-bind">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<div class="alert alert--danger hide" role="alert"></div>
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="30%"><%- @T('Name') %>
<th width="70%"><%- @T('Value') %>
</thead>
<tbody>
<tr>
<td class="settings-list-row-control"><%- @T('Endpoint') %>
<td class="settings-list-control-cell"><input type="text" name="endpoint" class="form-control form-control--small" value="" placeholder="https://outlook.office365.com/EWS/Exchange.asmx" autocomplete="off" required>
<tr>
<td class="settings-list-row-control"><%- @T('User') %>
<td class="settings-list-control-cell"><input type="text" name="user" class="form-control form-control--small js-user" value="" placeholder="" autocomplete="off" required>
<tr>
<td class="settings-list-row-control"><%- @T('Password') %>
<td class="settings-list-control-cell"><input type="password" name="password" class="form-control form-control--small js-password" value="" placeholder="" autocomplete="new-password" required>
</tbody>
</table>
</div>
</div>
<div class="modal-footer">
<div class="modal-rightFooter">
<button class="btn btn--primary align-right js-submit"><%- @T('Connect') %></button>
</div>
</div>
</form>
<form class="modal-content setup wizard hide js-folders">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Folders') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<div class="alert alert--danger hide" role="alert"></div>
<div class="column_select form-group">
<div class="formGroup-label">
<label for="folders"><%- @T('Import %s', 'Folders') %></label>
</div>
<div class="controls js-foldersSelect">
</div>
</div>
</div>
</div>
<div class="modal-footer">
<div class="modal-rightFooter">
<a class="btn btn--text btn--secondary js-goToSlide align-left" data-slide="js-bind"><%- @T('Go Back') %></a>
</div>
<div class="modal-rightFooter">
<button class="btn btn--primary align-right js-submitTry is-disabled"><%- @T('Continue') %></button>
</div>
</div>
</form>
<form class="modal-content setup wizard hide js-analyze">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<p class="wizard-loadingText">
<span class="loading icon"></span> <%- @T('Analyzing structure...') %>
</p>
</div>
</div>
<div class="modal-footer"></div>
</form>
<div class="modal-content setup wizard hide js-mapping">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Mapping') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<div class="alert alert--danger hide" role="alert"></div>
<h2><%- @T('User') %></h2>
<form class="js-userMappingForm">
<table class="settings-list js-userAttributeMap" style="width: 100%;">
<colgroup>
<col width="240">
<col>
<col>
</colgroup>
<thead>
<tr>
<th><%- @T('%s Attribute', 'Exchange') %>
<th><%- @T('%s Attribute', 'Zammad') %>
<th><%- @T('Action') %>
</thead>
<tbody>
<tr>
<td class="settings-list-row-control" colspan="3">
<div class="btn btn--text btn--create js-add">
<%- @Icon('plus-small') %> <%- @T('Add') %>
</div>
</tbody>
</table>
</form>
</div>
</div>
<div class="modal-footer">
<div class="modal-rightFooter">
<a class="btn btn--text btn--secondary js-goToSlide align-left" data-slide="js-bind"><%- @T('Go Back') %></a>
</div>
<div class="modal-rightFooter">
<button class="btn btn--primary align-right js-submitTry"><%- @T('Continue') %></button>
</div>
</div>
</div>
<form class="modal-content setup wizard hide js-dry">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<div class="js-preprogress">
<p class="wizard-loadingText">
<span class="loading icon"></span>
<%- @T('Counting entries. This may take a while.') %>
</p>
</div>
<div class="js-analyzing hide">
<p class="wizard-loadingText">
<%- @T('Analyzing entries with given configuration...') %>
</p>
<div class="centered js-progress">
<progress max="" value=""></progress>
</div>
</div>
</div>
</div>
<div class="modal-footer"></div>
</form>
<div class="modal-content setup wizard hide js-try">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %> <%- @T('Configuration') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<div class="alert alert--danger hide" role="alert"></div>
<p><%- @T('With your current configuration the following will happen') %>:</p>
<div class="js-summary"></div>
</div>
</div>
<div class="modal-footer">
<div class="modal-rightFooter">
<a class="btn btn--text btn--secondary js-goToSlide align-left" data-slide="js-mapping"><%- @T('Go Back') %></a>
</div>
<div class="modal-rightFooter">
<button class="btn btn--primary align-right js-submitSave"><%- @T('Save configuration') %></button>
</div>
</div>
</div>
<form class="modal-content setup wizard hide js-error">
<div class="modal-header">
<div class="modal-close js-close">
<%- @Icon('diagonal-cross') %>
</div>
<h1 class="modal-title"><%- @T('Exchange') %></h1>
</div>
<div class="modal-body">
<div class="wizard-body vertical justified">
<div class="alert alert--danger hide" role="alert"></div>
</div>
</div>
<div class="modal-footer">
<div class="modal-rightFooter">
<button class="btn btn--primary align-right"><%- @T('Cancel') %></button>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,25 @@
<form>
<h2><%- @T('Settings') %></h2>
<div class="settings-entry">
<table class="settings-list" style="width: 100%;">
<thead>
<tr>
<th width="20%"><%- @T('Name') %>
<th width="80%"><%- @T('Value') %>
</thead>
<tbody>
<tr>
<td class="settings-list-row-control"><%- @T('API token') %> *
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.api_token %>" name="api_token">
<tr>
<td class="settings-list-row-control"><%- @T('Endpoint') %> *
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.endpoint %>" placeholder="https://idoit.example.com/i-doit/" name="endpoint">
<tr>
<td class="settings-list-row-control"><%- @T('Client ID') %>
<td class="settings-list-control-cell"><input type="text" class="form-control form-control--small" value="<%= @config.client_id %>" name="client_id">
</tbody>
</table>
</div>
<button type="submit" class="btn btn--primary js-submit"><%- @T('Save') %></button>
</form>

View file

@ -0,0 +1,25 @@
<hr>
<% if _.isEmpty(@items): %>
<%- @T('none') %>
<% else: %>
<table class="table">
<thead>
<th style="width: 30px"></th>
<th style="width: 50px"><%- @T('ID') %></th>
<th><%- @T('Name') %></th>
<th><%- @T('Status') %></th>
<th><%- @T('Link') %></th>
</thead>
<tbody>
<% for item in @items: %>
<tr>
<td><input type="checkbox" name="object_id" value="<%= item.id %>"/></td>
<td><%= item.id %></td>
<td><%= item.title %></td>
<td><%= item.cmdb_status_title %></td>
<td><a href="<%- item.link %>" target="_blank">i-doit</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>

View file

@ -0,0 +1,5 @@
<form class="js-search flex horizontal">
<div class="controls controls--select js-typeSelect"></div>
<input type="text" name="title" value="" autocomplete="off" placeholder="<%- @Ti('Search') %>" class="form-control"/>
</form>
<form class="js-result"></form>

View file

@ -51,6 +51,10 @@
<label for="id1">Name</label> <label for="id1">Name</label>
<input id="id1" class="form-control" type="text" placeholder="Text Input"> <input id="id1" class="form-control" type="text" placeholder="Text Input">
</div> </div>
<div class="input form-group">
<label for="id1">Name (readonly)</label>
<input id="id1" class="form-control" type="text" placeholder="Text Input" readonly value="Sandor Clegane">
</div>
<div class="input form-group"> <div class="input form-group">
<label for="id2">Password</label> <label for="id2">Password</label>
<input id="id2" class="form-control" type="password" value="Password Input"> <input id="id2" class="form-control" type="password" value="Password Input">

View file

@ -116,8 +116,8 @@
</div> </div>
<div class="searchfield"> <div class="searchfield">
<%- @Icon('magnifier') %>
<input class="js-search form-control" name="search" placeholder="Search for users" type="search"> <input class="js-search form-control" name="search" placeholder="Search for users" type="search">
<%- @Icon('magnifier') %>
</div> </div>
<div class="userSearch horizontal"> <div class="userSearch horizontal">

View file

@ -24,7 +24,7 @@
<div class="formGroup-label"> <div class="formGroup-label">
<label for="password"><%- @Ti('Password') %></label> <label for="password"><%- @Ti('Password') %></label>
</div> </div>
<input id="password" name="password" type="password" class="form-control"/> <input id="password" name="password" type="password" class="form-control" autocomplete="off"/>
</div> </div>
<div class="form-group"> <div class="form-group">

View file

@ -0,0 +1,34 @@
<div class="page-header">
<div class="page-header-title"><h1><%- @T('Out of Office') %></h1></div>
</div>
<form class="action">
<div class="action-flow action-flow--row">
<div class="action-row">
<div class="action-flow action-flow--noWrap">
<h2><span class="action-form-status">
<% if @localData.out_of_office is true: %>
<%- @Icon('status', 'ok inline') %>
<% else: %>
<%- @Icon('status', 'error inline') %>
<% end %></span> <input id="out_of_office_reason" name="out_of_office_text" class="form-control form-control--inline" type="text" placeholder="<%- @Ti('e. g.') %> <%- @Ti(@placeholder) %>" value="<% if !_.isEmpty(@localData.out_of_office_text): %><%= @localData.out_of_office_text %><% end %>"></h2>
</div>
</div>
<div class="action-block action-block--flex">
<div class="label"><%- @T('From') %></div>
<div class="form-group js-startDate"></div>
</div>
<div class="action-block action-block--flex">
<div class="label"><%- @T('Till') %></div>
<div class="form-group js-endDate"></div>
</div>
<div class="action-row">
<label for="out_of_office_replacement"><%- @T('Replacement') %></label>
<div class="dropdown js-recipientDropdown"></div>
</div>
<div class="action-controls">
<div class="btn btn--danger js-disabled"><%- @Ti('Disable') %></div>
<div class="btn btn--create js-enable"><%- @Ti('Enable') %></div>
</div>
</div>
</form>

View file

@ -3,8 +3,8 @@
<div class="detail-search"> <div class="detail-search">
<div class="detail-search-header"> <div class="detail-search-header">
<div class="searchfield"> <div class="searchfield">
<%- @Icon('magnifier') %>
<input class="js-search form-control<%= if !@query then ' is-empty' %>" name="query" placeholder="<%- @Ti('Find what you search. E. g. "search phrase"') %>" value="<%= @query %>" type="search" autocomplete="off"> <input class="js-search form-control<%= if !@query then ' is-empty' %>" name="query" placeholder="<%- @Ti('Find what you search. E. g. "search phrase"') %>" value="<%= @query %>" type="search" autocomplete="off">
<%- @Icon('magnifier') %>
<div class="empty-search js-emptySearch"> <div class="empty-search js-emptySearch">
<%- @Icon('diagonal-cross') %> <%- @Icon('diagonal-cross') %>
</div> </div>

View file

@ -18,6 +18,12 @@
<input type="text" name="proxy_password" value="<%= @proxy_password %>" class="form-control"> <input type="text" name="proxy_password" value="<%= @proxy_password %>" class="form-control">
</div> </div>
</div> </div>
<p class="help-text"><%- @T('No proxy for the following hosts.') %></p>
<div class="horizontal end">
<div class="form-item flex">
<input type="text" name="proxy_no" value="<%= @proxy_no %>" placeholder="localhost,127.0.0.1" class="form-control">
</div>
</div>
<div class="horizontal justify-end form-controls"> <div class="horizontal justify-end form-controls">
<button class="btn btn js-test"><%- @T('Test Connection') %></button> <button class="btn btn js-test"><%- @T('Test Connection') %></button>
<button class="btn btn--primary js-submit hide"><%- @T('Submit') %></button> <button class="btn btn--primary js-submit hide"><%- @T('Submit') %></button>

View file

@ -1,21 +1,23 @@
<div class="tabsSidebar-holder"> <div class="tabsSidebar-holder">
<div class="scrollPageHeader tabsSidebar-sidebarSpacer" style="right: <%- @scrollbarWidth %>px"> <div class="scrollPageHeader tabsSidebar-sidebarSpacer" style="<%- if @dir is 'rtl' then 'left' else 'right' %>: <%- @scrollbarWidth %>px">
<small><%- @C('ticket_hook') %> <span class="ticket-number"><%- @ticket.number %></span></small> <small><%- @C('ticket_hook') %> <span class="ticket-number"><%- @ticket.number %></span></small>
<div class="ticket-title"></div> <div class="js-ticketTitleContainer ticket-title"></div>
<div class="highlighter"></div> <div class="js-highlighterContainer highlighter"></div>
<div class="overview-navigator"></div> <div class="js-overviewNavigatorContainer overview-navigator"></div>
</div> </div>
<div class="main no-padding flex tabsSidebar-sidebarSpacer tabsSidebar-tabsSpacer"> <div class="main no-padding flex tabsSidebar-sidebarSpacer tabsSidebar-tabsSpacer">
<div class="ticketZoom"> <div class="ticketZoom">
<div class="ticketZoom-controls"> <div class="ticketZoom-controls">
<div class="highlighter"></div> <div class="js-settingContainer"></div>
<div class="overview-navigator"></div> <div class="spacer"></div>
<div class="js-highlighterContainer highlighter"></div>
<div class="js-overviewNavigatorContainer overview-navigator"></div>
</div> </div>
<div class="ticketZoom-header"> <div class="ticketZoom-header">
<div class="flex vertical center"> <div class="flex vertical center">
<div class="js-avatar"></div> <div class="js-avatar"></div>
<div class="ticket-title"></div> <div class="js-ticketTitleContainer ticket-title"></div>
<div class="ticket-meta"></div> <div class="js-ticketMetaContainer"></div>
</div> </div>
</div> </div>
<div class="ticket-article"></div> <div class="ticket-article"></div>

View file

@ -41,13 +41,19 @@
<div class="formGroup-label"> <div class="formGroup-label">
<label for=""><%- @T('To') %></label> <label for=""><%- @T('To') %></label>
</div> </div>
<div class="controls"><input id="" type="text" name="to" value="<%= @article.to %>" class="form-control js-mail-inputs" required="required"></div> <div class="controls"><input type="text" name="to" value="<%= @article.to %>" class="form-control js-mail-inputs" required="required"></div>
</div> </div>
<div class="input form-group"> <div class="input form-group">
<div class="formGroup-label"> <div class="formGroup-label">
<label for=""><%- @T('Cc') %></label> <label for=""><%- @T('Cc') %></label>
</div> </div>
<div class="controls"><input id="" type="text" name="cc" value="<%= @article.cc %>" class="form-control js-mail-inputs"></div> <div class="controls"><input type="text" name="cc" value="<%= @article.cc %>" class="form-control js-mail-inputs"></div>
</div>
<div class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Subject') %></label>
</div>
<div class="controls"><input type="text" name="subject" value="<%= @article.subject %>" class="form-control js-mail-inputs2"></div>
</div> </div>
<div class="textBubble js-writeArea"> <div class="textBubble js-writeArea">

View file

@ -0,0 +1,3 @@
<div class="btn btn--action js-setting centered">
<%- @Icon('cog', 'dropdown-icon') %>
</div>

View file

@ -0,0 +1,13 @@
<% for object in @objects: %>
<div class="sidebar-block">
<label class="horizontal">
<%- @T(object.title) %>
<div class="list-item-delete js-delete" data-object-id="<%= object.id %>" data-type="remove">
<%- @Icon('diagonal-cross') %>
</div>
</label>
<%- @T('ID') %>: <a href="<%- object.link %>" target="_blank"><%= object.id %><br></a>
<%- @T('Status') %>: <%= object.cmdb_status_title %><br>
<%- @T('Type') %>: <%= object.type_title %><br>
</div>
<% end %>

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