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

View file

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

View file

@ -1 +1 @@
2.3.1
2.4.1

View file

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

View file

@ -1,8 +1,8 @@
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 'activerecord-session_store'
@ -40,6 +40,7 @@ gem 'omniauth-gitlab'
gem 'omniauth-google-oauth2'
gem 'omniauth-linkedin-oauth2'
gem 'omniauth-twitter'
gem 'omniauth-microsoft-office365'
gem 'twitter'
gem 'telegramAPI'
@ -73,12 +74,16 @@ gem 'argon2'
gem 'writeexcel'
gem 'icalendar'
gem 'icalendar-recurrence'
gem 'browser'
# integrations
gem 'slack-notifier'
gem 'clearbit'
gem 'zendesk_api'
gem 'viewpoint'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git'
# event machine
gem 'eventmachine'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ class App.ChannelForm extends App.ControllerSubContent
'change .js-paramsSetting select': 'updateGroup'
elements:
'.js-paramsBlock': 'paramsBlock'
'.js-code': 'code'
'.js-paramsSetting': 'paramsSetting'
'.js-formSetting input': 'formSetting'
@ -43,7 +43,7 @@ class App.ChannelForm extends App.ControllerSubContent
@html element
@paramsBlock.each (i, block) ->
@code.each (i, block) ->
hljs.highlightBlock block
@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,6 +28,7 @@ class Index extends App.ControllerIntegrationBase
super
active = @$('.js-switch input').prop('checked')
if active
job_start = =>
@ajax(
id: 'jobs_config'
type: 'POST'
@ -37,6 +38,12 @@ class Index extends App.ControllerIntegrationBase
@render(true)
)
App.Delay.set(
job_start,
600,
'job_start',
)
class Form extends App.Controller
elements:
'.js-lastImport': 'lastImport'
@ -91,6 +98,7 @@ class Form extends App.Controller
processData: true
success: (data, status, xhr) =>
@render(true)
@lastResult()
)
startWizard: (e) =>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,6 +9,7 @@ class App.TicketCreate extends App.Controller
constructor: (params) ->
super
@sidebarState = {}
# define default type
@default_type = 'phone-in'
@ -91,6 +92,8 @@ class App.TicketCreate extends App.Controller
else
@$('[name="cc"]').closest('.form-group').addClass('hide')
App.TaskManager.touch(@task_key)
meta: =>
text = ''
if @articleAttributes
@ -228,7 +231,7 @@ class App.TicketCreate extends App.Controller
type = @$('[name="formSenderType"]').val()
if signature isnt undefined && signature.body && type is 'email-out'
signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get() })
signatureFinished = App.Utils.replaceTags(signature.body, { user: App.Session.get(), config: App.Config.all() })
body = @$('[data-name=body]')
if App.Utils.signatureCheck(body.html() || '', signatureFinished)
@ -330,27 +333,47 @@ class App.TicketCreate extends App.Controller
# show text module UI
@textModule = new App.WidgetTextModule(
el: @$('[data-name="body"]').parent()
)
new Sidebar(
el: @sidebar
params: @formDefault
textModule: @textModule
data:
config: App.Config.all()
user: App.Session.get()
)
$('#tags').tokenfield()
@sidebarWidget = new App.TicketCreateSidebar(
el: @sidebar
params: @formDefault
sidebarState: @sidebarState
task_key: @task_key
query: @query
)
if @formDefault.customer_id
callback = (customer) =>
@localUserInfoCallback(@formDefault, customer)
App.User.full(@formDefault.customer_id, callback)
# update taskbar with new meta data
App.TaskManager.touch(@task_key)
localUserInfo: (e) =>
return if !@sidebarWidget
params = App.ControllerForm.params($(e.target).closest('form'))
new Sidebar(
el: @sidebar
params: params
textModule: @textModule
if params.customer_id
callback = (customer) =>
@localUserInfoCallback(params, customer)
App.User.full(params.customer_id, callback)
return
@localUserInfoCallback(params)
localUserInfoCallback: (params, customer = {}) =>
@sidebarWidget.render(params)
@textModule.reload(
config: App.Config.all()
user: App.Session.get()
ticket:
customer: customer
)
cancel: (e) ->
@ -475,6 +498,10 @@ class App.TicketCreate extends App.Controller
# scroll to top
ui.scrollTo()
# add sidebar params
if ui.sidebarWidget
ui.sidebarWidget.commit(ticket_id: @id)
# access to group
for group_id, access of App.Session.get('group_ids')
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
requiredPermission: 'ticket.agent'
constructor: (params) ->
@ -618,6 +537,9 @@ class Router extends App.ControllerPermanent
if params.customer_id
split = "/customer/#{params.customer_id}"
if params.query
split = "/query/#{params.query}"
id = Math.floor( Math.random() * 99999 )
@navigate "#ticket/create/id/#{id}#{split}"
return
@ -628,6 +550,7 @@ class Router extends App.ControllerPermanent
article_id: params.article_id
type: params.type
customer_id: params.customer_id
query: params.query
id: params.id
App.TaskManager.execute(
@ -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/customer/:customer_id', Router, 'Routes')
App.Config.set('ticket/create/id/:id/customer/:customer_id', Router, 'Routes')
App.Config.set('ticket/create/id/:id/query/:query', Router, 'Routes')
# split ticket
App.Config.set('ticket/create/:ticket_id/:article_id', Router, 'Routes')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,9 @@ class App._CollectionSingletonBase
constructor: ->
@callbacks = {}
@counter = 0
@key = "collection-#{@event}"
# read from cache
cache = App.SessionStorage.get("collection-#{@event}")
cache = App.SessionStorage.get(@key)
if cache
@set(cache)
@ -73,6 +73,9 @@ class App._CollectionSingletonBase
callback: (data) =>
for counter, attr of @callbacks
callback = ->
attr.callback(data)
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
, 300, {trailing: false}
if @attribute.onChange
@shadow.on('change', =>
@attribute.onChange(@shadow.val())
)
render: ->
@values = []
_.each @options.attribute.options, (option) =>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -104,8 +104,9 @@
// break <blockquote> after enter on empty line
sel = window.getSelection()
if (sel) {
node = $(sel.anchorNode)
if (node.parent().is('blockquote')) {
if (node && node.parent() && node.parent().is('blockquote')) {
e.preventDefault()
document.execCommand('Insertparagraph')
document.execCommand('Outdent')
@ -113,6 +114,16 @@
}
}
// 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()
_this.paste('<br><br>')
return
}
}
// on zammad magicKey + i/b/u/s
// hotkeys + u -> Toggles the current selection between underlined and not underlined
// hotkeys + b -> Toggles the current selection between bold and non-bold
@ -295,7 +306,7 @@
else {
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
}
document.execCommand('insertHTML', false, img)
_this.paste(img)
}
// resize if to big
@ -367,13 +378,7 @@
text = App.Utils.removeEmptyLines(text)
_this.log('insert', text)
// as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
if (docType == 'text3') {
_this.pasteHtmlAtCaret(text)
}
else {
document.execCommand('insertHTML', false, text)
}
_this.paste(text)
return true
})
@ -533,37 +538,6 @@
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
Plugin.prototype.log = function() {
if (App && App.Log) {
@ -574,6 +548,29 @@
}
}
// paste some content
Plugin.prototype.paste = function(string) {
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
// IE <= 10
if (document.selection && document.selection.createRange) {
var range = document.selection.createRange()
if (range.pasteHTML) {
range.pasteHTML(string)
}
}
// IE == 11
else if (isIE11 && document.getSelection) {
var range = document.getSelection().getRangeAt(0)
var nnode = document.createElement('div')
range.surroundContents(nnode)
nnode.innerHTML = string
}
else {
document.execCommand('insertHTML', false, string)
}
}
$.fn[pluginName] = function (options) {
return this.each(function () {
if (!$.data(this, 'plugin_' + pluginName)) {
@ -586,6 +583,9 @@
// get correct val if textbox
$.fn.ceg = function() {
var plugin = $.data(this[0], 'plugin_' + pluginName)
if (!plugin) {
return
}
return plugin.value()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@
<div class="sidebar-content"></div>
</div>
<% 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: %>
<div class="tabsSidebar-tab" data-tab="<%= item.name %>">
<%- @Icon(item.icon) %>

View file

@ -18,7 +18,7 @@
<input type="checkbox" value="<%= role.id %>" name="role_ids" <% if @rolesSelected[role.id]: %>checked<% end %>/>
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @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>
<% if role.permissions: %>
<% for permission in role.permissions: %>

View file

@ -63,7 +63,7 @@
</form>
<form class="setup wizard hide js-inbound">
<div class="wizard-slide">
<div class="wizard-slide wizard-slide--large">
<h2><%- @T('Email Inbound') %></h2>
<div class="wizard-body vertical justified">
<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">
<h2><%- @T('%s Migration', 'Zendesk') %></h2>
<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">
<table class="progressTable">
<tr class="js-group">

View file

@ -10,9 +10,10 @@
<div class="page-content">
<% if @description: %>
<% for item in @description: %>
<p><%- @T(item[0], item[1], item[2]) %></p>
<p><%- @T(item...) %></p>
<% end %>
<% end %>
<div class="js-form"></div>
<div class="js-scriptSnipped"></div>
<div class="js-log"></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>
<input id="id1" class="form-control" type="text" placeholder="Text Input">
</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">
<label for="id2">Password</label>
<input id="id2" class="form-control" type="password" value="Password Input">

View file

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

View file

@ -24,7 +24,7 @@
<div class="formGroup-label">
<label for="password"><%- @Ti('Password') %></label>
</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 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-header">
<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">
<%- @Icon('magnifier') %>
<div class="empty-search js-emptySearch">
<%- @Icon('diagonal-cross') %>
</div>

View file

@ -18,6 +18,12 @@
<input type="text" name="proxy_password" value="<%= @proxy_password %>" class="form-control">
</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">
<button class="btn btn js-test"><%- @T('Test Connection') %></button>
<button class="btn btn--primary js-submit hide"><%- @T('Submit') %></button>

View file

@ -1,21 +1,23 @@
<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>
<div class="ticket-title"></div>
<div class="highlighter"></div>
<div class="overview-navigator"></div>
<div class="js-ticketTitleContainer ticket-title"></div>
<div class="js-highlighterContainer highlighter"></div>
<div class="js-overviewNavigatorContainer overview-navigator"></div>
</div>
<div class="main no-padding flex tabsSidebar-sidebarSpacer tabsSidebar-tabsSpacer">
<div class="ticketZoom">
<div class="ticketZoom-controls">
<div class="highlighter"></div>
<div class="overview-navigator"></div>
<div class="js-settingContainer"></div>
<div class="spacer"></div>
<div class="js-highlighterContainer highlighter"></div>
<div class="js-overviewNavigatorContainer overview-navigator"></div>
</div>
<div class="ticketZoom-header">
<div class="flex vertical center">
<div class="js-avatar"></div>
<div class="ticket-title"></div>
<div class="ticket-meta"></div>
<div class="js-ticketTitleContainer ticket-title"></div>
<div class="js-ticketMetaContainer"></div>
</div>
</div>
<div class="ticket-article"></div>

View file

@ -41,13 +41,19 @@
<div class="formGroup-label">
<label for=""><%- @T('To') %></label>
</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 class="input form-group">
<div class="formGroup-label">
<label for=""><%- @T('Cc') %></label>
</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 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 %>

View file

@ -13,6 +13,8 @@
<th><%- @T('Agent') %>
<th><%- @T('Time Units') %>
<th><%- @T('Time Units Total') %>
<th><%- @T('Created at') %>
<th><%- @T('Closed at') %>
</thead>
<tbody>
<% for row in @rows: %>
@ -24,6 +26,8 @@
<td><%= row.agent %>
<td><%= row.time_unit %>
<td><%= row.ticket.time_unit %>
<td><%- @humanTime(row.ticket.created_at) %>
<td><%- @humanTime(row.ticket.close_at) %>
<% end %>
</tbody>
</table>

View file

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

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