Merge branch 'develop' into cast-boolean-object-attribute

This commit is contained in:
Umar Sheikh 2018-02-04 20:11:09 +05:00
commit 49223a51c7
946 changed files with 22736 additions and 10335 deletions

View file

@ -1,6 +1,18 @@
<!--
Hi there - thanks for filling an issue. Please ensure the following things before creating an issue - thank you! 🤓
Hi there - thanks for filing an issue. Please ensure the following things before creating an issue - thank you! 🤓
Since november 15th we handle all requests, except real bugs, at our community board.
Full explanation: https://community.zammad.org/t/major-change-regarding-github-issues-community-board/21
Please post:
- Feature requests
- Development questions
- Technical questions
on the board -> https://community.zammad.org !
If you think you hit a bug, please continue:
- Search existing issues and the CHANGELOG.md for your issue - there might be a solution already
- Make sure to use the latest version of Zammad if possible
- Add the `log/production.log` file from your system. Attention: Make sure no confidential data is in it!
@ -8,30 +20,33 @@ Hi there - thanks for filling an issue. Please ensure the following things befor
- Don't remove the template - otherwise we will close the issue without further comments
- Ask questions about Zammad configuration and usage at our mailinglist. See: https://zammad.org/participate
Note: We always do our best. Unfortunately, sometimes the requests are too much and we can't handle everything at once. If you want to prioritize/escalate your issue, you can do so by means of a support contract (see https://zammad.com/pricing#selfhosted).
Note: We always do our best. Unfortunately, sometimes there are too many requests and we can't handle everything at once. If you want to prioritize/escalate your issue, you can do so by means of a support contract (see https://zammad.com/pricing#selfhosted).
* The upper textblock will be removed automatically when you submit your issue *
-->
### Infos:
* Used Zammad version:
* Used Zammad installation source: (source, package, ...)
* Operating system:
* Browser + version:
* Used Zammad version:
* Installation method (source, package, ..):
* Operating system:
* Database + version:
* Elasticsearch version:
* Browser + version:
### Expected behavior:
*
*
### Actual behavior:
*
*
### Steps to reproduce the behavior:
*
*
Yes I'm sure this is a bug and no feature request or a general question.

View file

@ -310,7 +310,8 @@ test:integration:es_mysql:
- ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- ruby -I test/ test/controllers/user_organization_controller_test.rb
- ruby -I test/ test/controllers/user_controller_test.rb
- ruby -I test/ test/controllers/organization_controller_test.rb
- rake db:drop
test:integration:es_postgresql:
@ -328,7 +329,8 @@ test:integration:es_postgresql:
- ruby -I test/ test/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_test.rb
- ruby -I test/ test/controllers/user_organization_controller_test.rb
- ruby -I test/ test/controllers/user_controller_test.rb
- ruby -I test/ test/controllers/organization_controller_test.rb
- rake db:drop
test:integration:zendesk_mysql:
@ -355,24 +357,36 @@ test:integration:zendesk_postgresql:
- ruby -I test/ test/integration/zendesk_import_test.rb
- rake db:drop
test:integration:otrs_5_mysql:
test:integration:otrs_6_mysql:
stage: test
tags:
- core
- mysql
script:
- export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"
- export IMPORT_OTRS_ENDPOINT="http://vz1185.test.znuny.com/otrs/public.pl?Action=ZammadMigrator"
- rake db:create
- rake db:migrate
- ruby -I test/ test/integration/otrs_import_test.rb
- rake db:drop
test:integration:otrs_5_postgresql:
test:integration:otrs_6_postgresql:
stage: test
tags:
- core
- postgresql
script:
- export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz1185.test.znuny.com/otrs/public.pl?Action=ZammadMigrator"
- rake db:create
- rake db:migrate
- ruby -I test/ test/integration/otrs_import_test.rb
- rake db:drop
test:integration:otrs_5:
stage: test
tags:
- core
script:
- export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"

View file

@ -45,29 +45,29 @@ Style/TrailingCommaInArguments:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
Enabled: false
Style/SpaceInsideParens:
Layout/SpaceInsideParens:
Description: 'No spaces after ( or before ).'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
Enabled: false
Style/SpaceAfterMethodName:
Layout/SpaceAfterMethodName:
Description: >-
Do not put a space between a method name and the opening
parenthesis in a method definition.
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
Enabled: false
Style/LeadingCommentSpace:
Layout/LeadingCommentSpace:
Description: 'Comments should start with a space.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
Enabled: false
Style/MethodCallParentheses:
Style/MethodCallWithoutArgsParentheses:
Description: 'Do not use parentheses for method calls with no arguments.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
Enabled: false
Style/SpaceInsideBrackets:
Layout/SpaceInsideBrackets:
Description: 'No spaces after [ or before ].'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
Enabled: false
@ -83,19 +83,19 @@ Style/MethodDefParentheses:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
Enabled: false
Style/EmptyLinesAroundClassBody:
Layout/EmptyLinesAroundClassBody:
Description: "Keeps track of empty lines around class bodies."
Enabled: false
Style/EmptyLinesAroundMethodBody:
Layout/EmptyLinesAroundMethodBody:
Description: "Keeps track of empty lines around method bodies."
Enabled: false
Style/EmptyLinesAroundBlockBody:
Layout/EmptyLinesAroundBlockBody:
Description: "Keeps track of empty lines around block bodies."
Enabled: false
Style/EmptyLinesAroundModuleBody:
Layout/EmptyLinesAroundModuleBody:
Description: "Keeps track of empty lines around module bodies."
Enabled: false
@ -143,17 +143,29 @@ Rails/HasAndBelongsToMany:
# StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through'
Enabled: false
Rails/SkipsModelValidations:
Description: >-
Use methods that skips model validations with caution.
See reference for more information.
Reference: 'http://guides.rubyonrails.org/active_record_validations.html#skipping-validations'
Enabled: true
Exclude:
- test/**/*
Style/ClassAndModuleChildren:
Description: 'Checks style of children classes and modules.'
Enabled: false
Style/FileName:
Naming/FileName:
Description: 'Use snake_case for source file names.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
Enabled: true
Exclude:
- 'script/websocket-server.rb'
Naming/VariableNumber:
Description: 'Use the configured style when numbering variables.'
Enabled: false
# 2.0
@ -184,8 +196,23 @@ Metrics/ModuleLength:
Description: 'Avoid modules longer than 100 lines of code.'
Enabled: false
Metrics/BlockLength:
Enabled: false
Lint/RescueWithoutErrorClass:
Enabled: false
Rails/ApplicationRecord:
Enabled: false
# TODO
Rails/HasManyOrHasOneDependent:
Enabled: false
Style/DateTime:
Enabled: false
Style/Documentation:
Description: 'Document classes and non-namespace modules.'
Enabled: false
@ -193,7 +220,7 @@ Style/Documentation:
Lint/UselessAssignment:
Enabled: false
Style/ExtraSpacing:
Layout/ExtraSpacing:
Description: 'Do not use unnecessary spacing.'
Enabled: false
@ -215,4 +242,14 @@ Style/NumericPredicate:
AutoCorrect: false
Enabled: true
Exclude:
- "**/*_spec.rb"
- "**/*_spec.rb"
Lint/AmbiguousBlockAssociation:
Description: >-
Checks for ambiguous block association with method when param passed without
parentheses.
StyleGuide: '#syntax'
Enabled: true
Exclude:
- "**/*_spec.rb"
- "**/*_examples.rb"

View file

@ -1 +1 @@
2.4.1
2.4.2

View file

@ -19,7 +19,7 @@ services:
- mysql
language: ruby
rvm:
- 2.4.1
- 2.4.2
before_install:
- git fetch --unshallow
- sudo apt-get -qq update
@ -62,3 +62,4 @@ script:
after_success:
- if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-build.sh; fi
- if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-compose-build.sh; fi
- if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-univention-build.sh; fi

View file

@ -1,7 +1,7 @@
# Change Log
## [2.2.0](https://github.com/zammad/zammad/tree/2.2.0) (2017-xx-xx)
[Full Changelog](https://github.com/zammad/zammad/compare/2.1.0...2.2.0)
## [2.4.0](https://github.com/zammad/zammad/tree/2.4.0) (2018-xx-xx)
[Full Changelog](https://github.com/zammad/zammad/compare/2.3.0...2.4.0)
**Implemented enhancements:**

133
Gemfile
View file

@ -1,111 +1,131 @@
source 'https://rubygems.org'
ruby '2.4.1'
# core - base
ruby '2.4.2'
gem 'rails', '5.1.4'
gem 'rails-observers'
# core - rails additions
gem 'activerecord-session_store'
# Bundle edge Rails instead:
#gem 'rails', :git => 'git://github.com/rails/rails.git'
gem 'composite_primary_keys'
gem 'json'
gem 'rails-observers'
# Supported DBs
# core - application servers
gem 'puma', group: :puma
gem 'unicorn', group: :unicorn
# core - supported ORMs
gem 'activerecord-nulldb-adapter', group: :nulldb
gem 'mysql2', group: :mysql
gem 'pg', group: :postgres
# core - asynchrous task execution
gem 'daemons'
gem 'delayed_job_active_record'
# core - websocket
gem 'em-websocket'
gem 'eventmachine'
# core - password security
gem 'argon2'
# performance - Memcached
gem 'dalli'
# asset handling
group :assets do
gem 'sass-rails' #, github: 'rails/sass-rails'
# asset handling - coffee-script
gem 'coffee-rails'
gem 'coffee-script-source'
gem 'sprockets'
gem 'uglifier'
# asset handling - frontend templating
gem 'eco'
# asset handling - SASS
gem 'sass-rails'
# asset handling - pipeline
gem 'sprockets'
gem 'uglifier'
end
gem 'autoprefixer-rails'
# asset handling - javascript execution for e.g. linux
gem 'execjs'
gem 'libv8'
gem 'therubyracer'
# authentication - provider
gem 'doorkeeper'
gem 'oauth2'
# authentication - third party
gem 'omniauth'
gem 'omniauth-oauth2'
gem 'omniauth-facebook'
gem 'omniauth-github'
gem 'omniauth-gitlab'
gem 'omniauth-google-oauth2'
gem 'omniauth-linkedin-oauth2'
gem 'omniauth-twitter'
gem 'omniauth-microsoft-office365'
gem 'omniauth-oauth2'
gem 'omniauth-twitter'
gem 'omniauth-weibo-oauth2'
gem 'twitter'
gem 'telegramAPI'
# channels
gem 'koala'
gem 'mail'
gem 'valid_email2'
gem 'telegramAPI'
gem 'twitter'
# channels - email additions
gem 'htmlentities'
gem 'mail', '2.6.6'
gem 'mime-types'
gem 'valid_email2'
# feature - business hours
gem 'biz'
gem 'composite_primary_keys'
gem 'delayed_job_active_record'
gem 'daemons'
gem 'simple-rss'
# e. g. on linux we need a javascript execution
gem 'libv8'
gem 'execjs'
gem 'therubyracer'
require 'erb'
require 'yaml'
gem 'net-ldap'
# password security
gem 'argon2'
# feature - signature diffing
gem 'diffy'
# feature - excel output
gem 'writeexcel'
gem 'icalendar'
gem 'icalendar-recurrence'
# feature - device logging
gem 'browser'
# feature - iCal export
gem 'icalendar'
gem 'icalendar-recurrence'
# integrations
gem 'slack-notifier'
gem 'clearbit'
gem 'net-ldap'
gem 'slack-notifier'
gem 'zendesk_api'
gem 'viewpoint'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
# integrations - exchange
gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git'
# event machine
gem 'eventmachine'
gem 'em-websocket'
gem 'diffy'
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
gem 'viewpoint'
# Gems used only for develop/test and not required
# in production environments by default.
group :development, :test do
# test frameworks
gem 'rspec-rails'
gem 'test-unit'
gem 'spring'
gem 'spring-commands-rspec'
# test DB
gem 'sqlite3'
# code coverage
gem 'coveralls', require: false
gem 'simplecov'
gem 'simplecov-rcov'
gem 'coveralls', require: false
# UI tests w/ Selenium
gem 'selenium-webdriver', '2.53.4'
@ -120,9 +140,9 @@ group :development, :test do
gem 'guard-symlink', require: false
# code QA
gem 'coffeelint'
gem 'pre-commit'
gem 'rubocop'
gem 'coffeelint'
# changelog generation
gem 'github_changelog_generator'
@ -130,17 +150,14 @@ group :development, :test do
# Setting ENV for testing purposes
gem 'figaro'
# Use Factory Girl for generating random test data
gem 'factory_girl_rails'
# Use Factory Bot for generating random test data
gem 'factory_bot_rails'
# mock http calls
gem 'webmock'
end
gem 'puma', group: :puma
gem 'unicorn', group: :unicorn
# load onw gem's
# load onw gems for development and testing purposes
local_gemfile = File.join(File.dirname(__FILE__), 'Gemfile.local')
if File.exist?(local_gemfile)
eval_gemfile local_gemfile

View file

@ -65,22 +65,22 @@ GEM
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
arel (8.0.0)
argon2 (1.1.3)
argon2 (1.1.4)
ffi (~> 1.9)
ffi-compiler (~> 0.1)
ast (2.3.0)
autoprefixer-rails (7.1.3)
autoprefixer-rails (7.1.6)
execjs
biz (1.7.0)
clavius (~> 1.0)
tzinfo
browser (2.5.1)
browser (2.5.2)
buftok (0.2.0)
builder (3.2.3)
childprocess (0.7.1)
childprocess (0.8.0)
ffi (~> 1.0, >= 1.0.11)
clavius (1.0.3)
clearbit (0.2.7)
clearbit (0.2.8)
nestful (~> 1.1.0)
coderay (1.1.2)
coffee-rails (4.2.2)
@ -90,22 +90,24 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
coffeelint (1.16.0)
coffeelint (1.16.1)
coffee-script
execjs
json
composite_primary_keys (10.0.0)
composite_primary_keys (10.0.1)
activerecord (~> 5.1.0)
concurrent-ruby (1.0.5)
coveralls (0.8.21)
json (>= 1.8, < 3)
simplecov (~> 0.14.1)
term-ansicolor (~> 1.3)
thor (~> 0.19.4)
tins (~> 1.6)
coveralls (0.7.1)
multi_json (~> 1.3)
rest-client
simplecov (>= 0.7)
term-ansicolor
thor
crack (0.4.3)
safe_yaml (~> 1.0.0)
daemons (1.2.4)
crass (1.0.3)
daemons (1.2.5)
dalli (2.7.6)
delayed_job (4.1.3)
activesupport (>= 3.0, < 5.2)
delayed_job_active_record (4.1.2)
@ -127,15 +129,15 @@ GEM
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
equalizer (0.0.11)
erubi (1.6.1)
erubi (1.7.0)
eventmachine (1.2.5)
execjs (2.7.0)
factory_girl (4.8.0)
factory_bot (4.8.2)
activesupport (>= 3.0.0)
factory_girl_rails (4.8.0)
factory_girl (~> 4.8.0)
factory_bot_rails (4.8.2)
factory_bot (~> 4.8.2)
railties (>= 3.0.0)
faraday (0.11.0)
faraday (0.12.2)
multipart-post (>= 1.2, < 3)
faraday-http-cache (2.0.0)
faraday (~> 0.8)
@ -154,7 +156,7 @@ GEM
rainbow (>= 2.1)
rake (>= 10.0)
retriable (~> 2.1)
globalid (0.4.0)
globalid (0.4.1)
activesupport (>= 4.2.0)
guard (2.14.1)
formatador (>= 0.2.4)
@ -174,20 +176,21 @@ GEM
guard-symlink (0.1.1)
guard
guard-compat (~> 1.1)
hashdiff (0.3.6)
hashdiff (0.3.7)
hashie (3.5.6)
htmlentities (4.3.4)
http (2.2.2)
http (3.0.0)
addressable (~> 2.3)
http-cookie (~> 1.0)
http-form_data (~> 1.0.1)
http-form_data (>= 2.0.0.pre.pre2, < 3)
http_parser.rb (~> 0.6.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http-form_data (1.0.3)
http-form_data (2.0.0)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (0.8.6)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
icalendar (2.4.1)
icalendar-recurrence (1.1.2)
icalendar (~> 2.0)
@ -210,25 +213,26 @@ GEM
logging (2.2.2)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.0.3)
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lumberjack (1.0.12)
mail (2.6.6)
mime-types (>= 1.16, < 4)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
method_source (0.9.0)
mime-types (2.99.3)
mini_portile2 (2.3.0)
minitest (5.10.3)
multi_json (1.12.2)
multi_xml (0.6.0)
multipart-post (2.0.0)
mysql2 (0.4.9)
mysql2 (0.4.10)
naught (1.1.0)
nenv (0.3.0)
nestful (1.1.1)
net-ldap (0.16.0)
nestful (1.1.3)
net-ldap (0.16.1)
netrc (0.11.0)
nio4r (2.1.0)
nokogiri (1.8.1)
@ -246,7 +250,7 @@ GEM
rack (>= 1.2, < 3)
octokit (4.7.0)
sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.6.1)
omniauth (1.7.1)
hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3)
omniauth-facebook (4.0.0)
@ -280,24 +284,24 @@ GEM
omniauth-weibo-oauth2 (0.4.5)
omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0)
parser (2.4.0.0)
ast (~> 2.2)
parallel (1.12.0)
parser (2.4.0.2)
ast (~> 2.3)
pg (0.21.0)
pluginator (1.5.0)
power_assert (1.1.0)
power_assert (1.1.1)
powerpack (0.1.1)
pre-commit (0.35.0)
pre-commit (0.37.0)
pluginator (~> 1.5)
pry (0.10.4)
pry (0.11.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
public_suffix (3.0.0)
puma (3.10.0)
method_source (~> 0.9.0)
public_suffix (3.0.1)
puma (3.11.0)
rack (2.0.3)
rack-livereload (0.3.16)
rack
rack-test (0.7.0)
rack-test (0.8.2)
rack (>= 1.0, < 3)
rails (5.1.4)
actioncable (= 5.1.4)
@ -327,7 +331,7 @@ GEM
rainbow (2.2.2)
rake
raindrops (0.19.0)
rake (12.1.0)
rake (12.3.0)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2)
@ -337,39 +341,40 @@ GEM
mime-types (>= 1.16, < 3.0)
netrc (~> 0.7)
retriable (2.1.0)
rspec-core (3.6.0)
rspec-support (~> 3.6.0)
rspec-expectations (3.6.0)
rspec-core (3.7.0)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-mocks (3.6.0)
rspec-support (~> 3.7.0)
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
rspec-rails (3.6.1)
rspec-support (~> 3.7.0)
rspec-rails (3.7.2)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.6.0)
rspec-expectations (~> 3.6.0)
rspec-mocks (~> 3.6.0)
rspec-support (~> 3.6.0)
rspec-support (3.6.0)
rubocop (0.42.0)
parser (>= 2.3.1.1, < 3.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
rspec-support (3.7.0)
rubocop (0.51.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.8.1)
ruby-progressbar (1.9.0)
ruby_dep (1.5.0)
rubyzip (1.2.1)
safe_yaml (1.0.4)
sass (3.5.1)
sass (3.5.3)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.0.6)
sass-rails (5.0.7)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
@ -383,9 +388,8 @@ GEM
rubyzip (~> 1.0)
websocket (~> 1.0)
shellany (0.0.1)
simple-rss (1.3.1)
simple_oauth (0.3.1)
simplecov (0.14.1)
simplecov (0.15.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
@ -393,11 +397,6 @@ GEM
simplecov-rcov (0.2.3)
simplecov (>= 0.4.1)
slack-notifier (2.3.1)
slop (3.6.0)
spring (2.0.2)
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
@ -410,26 +409,27 @@ GEM
rest-client (~> 1.7, >= 1.7.3)
term-ansicolor (1.6.0)
tins (~> 1.0)
test-unit (3.2.5)
test-unit (3.2.6)
power_assert
therubyracer (0.12.3)
libv8 (~> 3.16.14.15)
ref
thor (0.19.4)
thor (0.20.0)
thread_safe (0.3.6)
tilt (2.0.8)
tins (1.15.0)
twitter (6.1.0)
addressable (~> 2.5)
tins (1.15.1)
twitter (6.2.0)
addressable (~> 2.3)
buftok (~> 0.2.0)
equalizer (= 0.0.11)
faraday (~> 0.11.0)
http (~> 2.1)
equalizer (~> 0.0.11)
http (~> 3.0)
http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0)
memoizable (~> 0.4.2)
naught (~> 1.1)
simple_oauth (~> 0.3.1)
tzinfo (1.2.3)
memoizable (~> 0.4.0)
multipart-post (~> 2.0)
naught (~> 1.0)
simple_oauth (~> 0.3.0)
tzinfo (1.2.4)
thread_safe (~> 0.1)
uglifier (3.2.0)
execjs (>= 0.3.0, < 3)
@ -437,10 +437,10 @@ GEM
unf_ext
unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
unicorn (5.3.0)
unicorn (5.3.1)
kgio (~> 2.6)
raindrops (~> 0.7)
valid_email2 (2.0.1)
valid_email2 (2.1.0)
activemodel (>= 3.2)
mail (~> 2.5)
viewpoint (1.1.0)
@ -448,16 +448,16 @@ GEM
logging
nokogiri
rubyntlm
webmock (3.0.1)
webmock (3.1.1)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff
websocket (1.2.4)
websocket (1.2.5)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
websocket-extensions (0.1.3)
writeexcel (1.0.5)
zendesk_api (1.14.4)
zendesk_api (1.16.0)
faraday (~> 0.9)
hashie (>= 3.5.2, < 4.0.0)
inflection
@ -482,6 +482,7 @@ DEPENDENCIES
composite_primary_keys
coveralls
daemons
dalli
delayed_job_active_record
diffy
doorkeeper
@ -489,7 +490,7 @@ DEPENDENCIES
em-websocket
eventmachine
execjs
factory_girl_rails
factory_bot_rails
figaro
github_changelog_generator
guard
@ -501,7 +502,7 @@ DEPENDENCIES
json
koala
libv8
mail
mail (= 2.6.6)
mime-types
mysql2
net-ldap
@ -528,12 +529,9 @@ DEPENDENCIES
rubyntlm!
sass-rails
selenium-webdriver (= 2.53.4)
simple-rss
simplecov
simplecov-rcov
slack-notifier
spring
spring-commands-rspec
sprockets
sqlite3
telegramAPI
@ -549,7 +547,7 @@ DEPENDENCIES
zendesk_api
RUBY VERSION
ruby 2.4.1p111
ruby 2.4.2p198
BUNDLED WITH
1.15.4
1.16.0

View file

@ -3,9 +3,9 @@
Zammad is a web based open source helpdesk/customer support system with many
features to manage customer communication via several channels like telephone,
facebook, twitter, chat and e-mails. It is distributed under the GNU AFFERO
General Public License (AGPL) and tested on Linux, Solaris, AIX, FreeBSD,
OpenBSD and Mac OS 10.x. Do you receive many e-mails and want to answer them
with a team of agents?
General Public License (AGPL).
Do you receive many e-mails and want to answer them with a team of agents?
You're going to love Zammad!

0
Rakefile Normal file → Executable file
View file

View file

@ -1 +1 @@
2.2.x
2.4.x

View file

@ -700,6 +700,8 @@ class App.ControllerModal extends App.Controller
headPrefix: ''
shown: true
closeOnAnyClick: false
initalFormParams: {}
initalFormParamsIgnore: false
events:
'submit form': 'submit'
@ -746,10 +748,10 @@ class App.ControllerModal extends App.Controller
centerButtons: @centerButtons
leftButtons: @leftButtons
)
modal.find('.modal-body').html content
modal.find('.modal-body').html(content)
if !@initRenderingDone
@initRenderingDone = true
@html modal
@html(modal)
else
@$('.modal-dialog').replaceWith(modal)
@post()
@ -761,6 +763,8 @@ class App.ControllerModal extends App.Controller
@el
render: =>
@initalFormParamsIgnore = false
if @buttonSubmit is true
@buttonSubmit = 'Submit'
if @buttonCancel is true
@ -775,19 +779,18 @@ class App.ControllerModal extends App.Controller
if @small
@el.addClass('modal--small')
@el.modal
@el.modal(
keyboard: @keyboard
show: true
backdrop: @backdrop
container: @container
.on
'show.bs.modal': @onShow
'shown.bs.modal': @onShown
'hide.bs.modal': @onClose
'hidden.bs.modal': =>
@onClosed()
$('.modal').remove()
'dismiss.bs.modal': @onCancel
).on(
'show.bs.modal': @localOnShow
'shown.bs.modal': @localOnShown
'hide.bs.modal': @localOnClose
'hidden.bs.modal': @localOnClosed
'dismiss.bs.modal': @localOnCancel
)
if @closeOnAnyClick
@el.on('click', =>
@ -797,6 +800,7 @@ class App.ControllerModal extends App.Controller
close: (e) =>
if e
e.preventDefault()
@initalFormParamsIgnore = true
@el.modal('hide')
formParams: =>
@ -804,28 +808,50 @@ class App.ControllerModal extends App.Controller
return @formParam(@container.find('.modal form'))
return @formParam(@$('.modal form'))
onShow: ->
localOnShow: (e) =>
@onShow(e)
onShow: (e) ->
# do nothing
onShown: =>
localOnShown: (e) =>
@onShown(e)
onShown: (e) =>
@$('input:not([disabled]):not([type="hidden"]):not(".btn"), textarea').first().focus()
@initalFormParams = @formParams()
localOnClose: (e) =>
diff = difference(@initalFormParams, @formParams())
if @initalFormParamsIgnore is false && !_.isEmpty(diff)
if !confirm(App.i18n.translateContent('The form content has been changed. Do you want to close it and lose your changes?'))
e.preventDefault()
return
@onClose(e)
onClose: ->
# do nothing
onClosed: ->
localOnClosed: (e) =>
@onClosed(e)
$('.modal').remove()
onClosed: (e) ->
# do nothing
onSubmit: ->
# do nothing
localOnCancel: (e) =>
@onCancel(e)
onCancel: ->
onCancel: (e) ->
# do nothing
cancel: (e) =>
@close(e)
@onCancel(e)
onSubmit: (e) ->
# do nothing
submit: (e) =>
e.stopPropagation()
e.preventDefault()

View file

@ -86,6 +86,9 @@ class App.ControllerForm extends App.Controller
for attribute in @attributes
attribute_count = attribute_count + 1
if @isDisabled == true
attribute.disabled = true
# add item
item = @formGenItem(attribute, className, fieldset, attribute_count)
item.appendTo(fieldset)

View file

@ -148,24 +148,26 @@ class App.ControllerGenericIndex extends App.Controller
return item
)
# show description button, only if content exists
showDescription = false
if App[ @genericObject ].description && !_.isEmpty(objects)
showDescription = true
if !@table
@html App.view('generic/admin/index')(
head: @pageData.objects
notes: @pageData.notes
buttons: @pageData.buttons
menus: @pageData.menus
showDescription: showDescription
)
# show description button, only if content exists
showDescription = false
if App[ @genericObject ].description && !_.isEmpty(objects)
showDescription = true
# show description in content if no no content exists
if _.isEmpty(objects) && App[ @genericObject ].description
description = marked(App[ @genericObject ].description)
@$('.table-overview').html(description)
return
@html App.view('generic/admin/index')(
head: @pageData.objects
notes: @pageData.notes
buttons: @pageData.buttons
menus: @pageData.menus
showDescription: showDescription
)
# show description in content if no no content exists
if _.isEmpty(objects) && App[ @genericObject ].description
description = marked(App[ @genericObject ].description)
@$('.table-overview').html(description)
return
# append content table
params = _.extend(
@ -184,7 +186,10 @@ class App.ControllerGenericIndex extends App.Controller
},
@pageData.tableExtend
)
new App.ControllerTable(params)
if !@table
@table = new App.ControllerTable(params)
else
@table.update(objects: objects)
edit: (id, e) =>
e.preventDefault()
@ -651,7 +656,7 @@ class App.Sidebar extends App.Controller
'.sidebar': 'sidebars'
events:
'click .tabsSidebar-tab': 'toggleTab'
'click .tabsSidebar-tab': 'toggleTab'
'click .tabsSidebar-close': 'toggleSidebar'
'click .sidebar-header .js-headline': 'toggleDropdown'
@ -675,26 +680,48 @@ class App.Sidebar extends App.Controller
@toggleTabAction(name)
render: =>
itemsLocal = []
for item in @items
itemLocal = item.sidebarItem()
if itemLocal
itemsLocal.push itemLocal
# container
localEl = $(App.view('generic/sidebar_tabs')(
items: @items
items: itemsLocal
scrollbarWidth: App.Utils.getScrollBarWidth()
dir: App.i18n.dir()
))
# init content callback
for item in @items
area = localEl.filter('.sidebar[data-tab="' + item.name + '"]')
if item.callback
item.callback( area.find('.sidebar-content') )
if item.actions
new App.ActionRow(
el: area.find('.js-actions')
items: item.actions
type: 'small'
)
# init sidebar badget
for item in itemsLocal
el = localEl.find('.tabsSidebar-tab[data-tab="' + item.name + '"]')
if item.badgeCallback
item.badgeCallback(el)
else
@badgeRender(el, item)
# init sidebar content
for item in itemsLocal
if item.sidebarCallback
el = localEl.filter('.sidebar[data-tab="' + item.name + '"]')
item.sidebarCallback(el.find('.sidebar-content'))
if !_.isEmpty(item.sidebarActions)
new App.ActionRow(
el: el.find('.js-actions')
items: item.sidebarActions
type: 'small'
)
@html localEl
badgeRender: (el, item) =>
@badgeEl = el
@badgeRenderLocal(item)
badgeRenderLocal: (item) =>
@badgeEl.html(App.view('generic/sidebar_tabs_item')(icon: item.badgeIcon))
toggleDropdown: (e) ->
e.stopPropagation()
$(e.currentTarget).next('.js-actions').find('.dropdown-toggle').dropdown('toggle')
@ -1170,7 +1197,6 @@ class App.ObserverController extends App.Controller
if @globalRerender
@bind('ui:rerender', =>
@lastAttributres = undefined
console.log('aaaa', @model, @template)
@maybeRender(App[@model].fullLocal(@object_id))
)

View file

@ -97,6 +97,7 @@ class App.ControllerTable extends App.Controller
checkBoxColWidth: 40
radioColWidth: 22
sortableColWidth: 36
destroyColWidth: 70
elements:
'.js-tableHead': 'tableHead'
@ -133,6 +134,8 @@ class App.ControllerTable extends App.Controller
customOrderDirection: undefined
customOrderBy: undefined
frontendTimeUpdateExecute: true
bindCol: {}
bindRow: {}
@ -269,6 +272,7 @@ class App.ControllerTable extends App.Controller
@currentRows = newCurrentRows
@log 'debug', 'table.fullRender.contentRemoved', removePositions, addPositions
@renderPager(@el, true)
@frontendTimeUpdateElement(@el) if @frontendTimeUpdateExecute is true
return ['fullRender.contentRemoved', removePositions, addPositions]
if newRows.length isnt @currentRows.length
@ -304,6 +308,7 @@ class App.ControllerTable extends App.Controller
else
@currentRows = clone(rows)
container.find('.js-tableBody').html(rows)
@frontendTimeUpdateElement(container) if @frontendTimeUpdateExecute is true
@renderPager(container)
@ -506,6 +511,7 @@ class App.ControllerTable extends App.Controller
# get header data
@headers = []
availableWidth = @availableWidth
for item in @overviewAttributes
headerFound = false
for attributeName, attribute of @attributesList
@ -520,7 +526,7 @@ class App.ControllerTable extends App.Controller
# e.g. column: owner
headerFound = true
if @headerWidth[attribute.name]
attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth
attribute.displayWidth = @headerWidth[attribute.name] * availableWidth
else if !attribute.width
attribute.displayWidth = @baseColWidth
else
@ -529,7 +535,7 @@ class App.ControllerTable extends App.Controller
unit = attribute.width.match(/[px|%]+/)[0]
if unit is '%'
attribute.displayWidth = value / 100 * @el.width()
attribute.displayWidth = value / 100 * availableWidth
else
attribute.displayWidth = value
@headers.push attribute
@ -538,7 +544,7 @@ class App.ControllerTable extends App.Controller
if attributeName is "#{item}_id" || attributeName is "#{item}_ids"
headerFound = true
if @headerWidth[attribute.name]
attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth
attribute.displayWidth = @headerWidth[attribute.name] * availableWidth
else if !attribute.width
attribute.displayWidth = @baseColWidth
else
@ -547,7 +553,7 @@ class App.ControllerTable extends App.Controller
unit = attribute.width.match(/[px|%]+/)[0]
if unit is '%'
attribute.displayWidth = value / 100 * @el.width()
attribute.displayWidth = value / 100 * availableWidth
else
attribute.displayWidth = value
@headers.push attribute
@ -741,8 +747,10 @@ class App.ControllerTable extends App.Controller
if @availableWidth is 0
@availableWidth = @minTableWidth
availableWidth = @availableWidth
widths = @getHeaderWidths()
shrinkBy = Math.ceil (widths - @availableWidth) / @getShrinkableHeadersCount()
shrinkBy = Math.ceil (widths - availableWidth) / @getShrinkableHeadersCount()
# make all cols evenly smaller
@headers = _.map @headers, (col) =>
@ -751,7 +759,8 @@ class App.ControllerTable extends App.Controller
return col
# give left-over space from rounding to last column to get to 100%
roundingLeftOver = @availableWidth - @getHeaderWidths()
roundingLeftOver = availableWidth - @getHeaderWidths()
# but only if there is something left over (will get negative when there are too many columns for each column to stay in their min width)
if roundingLeftOver > 0
@headers[@headers.length - 1].displayWidth = @headers[@headers.length - 1].displayWidth + roundingLeftOver
@ -777,6 +786,9 @@ class App.ControllerTable extends App.Controller
if @dndCallback
widths += @sortableColWidth
if @destroy
widths += @destroyColWidth
widths
setHeaderWidths: =>

View file

@ -478,7 +478,6 @@ class App.ChannelEmailEdit extends App.ControllerModal
class App.ChannelEmailAccountWizard extends App.WizardModal
elements:
'.modal-body': 'body'
events:
'submit .js-intro': 'probeBasedOnIntro'
'submit .js-inbound': 'probeInbound'
@ -487,6 +486,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
'click .js-goToSlide': 'goToSlide'
'click .js-expert': 'probeBasedOnIntro'
'click .js-close': 'hide'
inboundPassword: ''
outboundPassword: ''
passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}'
constructor: ->
super
@ -503,9 +505,17 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
if @channel
@account =
inbound: @channel.options.inbound
outbound: @channel.options.outbound
meta: {}
inbound: clone(@channel.options.inbound)
outbound: clone(@channel.options.outbound)
meta: {}
# remember passwords, do not show in ui
if @account.inbound.options && @account.inbound.options.password
@inboundPassword = @account.inbound.options.password
@account.inbound.options.password = @passwordPlaceholder
if @account.outbound.options && @account.outbound.options.password
@outboundPassword = @account.outbound.options.password
@account.outbound.options.password = @passwordPlaceholder
if @container
@el.addClass('modal--local')
@ -515,17 +525,17 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
if @channel
@$('.js-goToSlide[data-slide=js-intro]').addClass('hidden')
@el.modal
@el.modal(
keyboard: true
show: true
backdrop: true
container: @container
.on
).on(
'hidden.bs.modal': =>
if @callback
@callback()
@el.remove()
)
if @slide
@showSlide(@slide)
@ -712,6 +722,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
# get params
params = @formParam(e.target)
if params.options.password is @passwordPlaceholder
params.options.password = @inboundPassword
# let backend know about the channel
if @channel
params.channel_id = @channel.id
@ -771,6 +784,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
params = @formParam(e.target)
params['email'] = @account['meta']['email']
if params.options.password is @passwordPlaceholder
params.options.password = @outboundPassword
if !params['email'] && @channel
email_addresses = App.EmailAddress.search(filter: { channel_id: @channel.id })
if email_addresses && email_addresses[0]
@ -867,11 +883,13 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
class App.ChannelEmailNotificationWizard extends App.WizardModal
elements:
'.modal-body': 'body'
events:
'change .js-outbound [name=adapter]': 'toggleOutboundAdapter'
'submit .js-outbound': 'probleOutbound'
'click .js-close': 'hide'
inboundPassword: ''
outboundPassword: ''
passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}'
constructor: ->
super
@ -888,27 +906,35 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
if @channel
@account =
inbound: @channel.options.inbound
outbound: @channel.options.outbound
inbound: clone(@channel.options.inbound)
outbound: clone(@channel.options.outbound)
# remember passwords, do not show in ui
if @account.inbound && @account.inbound.options && @account.inbound.options.password
@inboundPassword = @account.inbound.options.password
@account.inbound.options.password = @passwordPlaceholder
if @account.outbound && @account.outbound.options && @account.outbound.options.password
@outboundPassword = @account.outbound.options.password
@account.outbound.options.password = @passwordPlaceholder
if @container
@el.addClass('modal--local')
@render()
@el.modal
@el.modal(
keyboard: true
show: true
backdrop: true
container: @container
.on
).on(
'show.bs.modal': @onShow
'shown.bs.modal': @onShown
'hidden.bs.modal': =>
if @callback
@callback()
@el.remove()
)
if @slide
@showSlide(@slide)
@ -956,6 +982,9 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
# get params
params = @formParam(e.target)
if params.options && params.options.password is @passwordPlaceholder
params.options.password = @outboundPassword
# let backend know about the channel
params.channel_id = @channel.id

View file

@ -67,11 +67,15 @@ class App.ChannelForm extends App.ControllerSubContent
# rebuild preview
params.test = true
if params.modal
@$('.js-modal').removeClass('hide')
@$('.js-inlineForm').addClass('hide')
@$('.js-formInline').addClass('hide')
@$('.js-formBtn').removeClass('hide')
@$('.js-formBtn').ZammadForm(params)
@$('.js-formBtn').text('Feedback')
else
@$('.js-modal').addClass('hide')
@$('.js-inlineForm').removeClass('hide')
@$('.js-formBtn').addClass('hide')
@$('.js-formInline').removeClass('hide')
@$('.js-formInline').ZammadForm(params)

View file

@ -131,13 +131,12 @@ class Form extends App.Controller
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))
el = $(App.view('integration/exchange_last_import')(job: job))
@lastImport.html(el)
activeDryRun: =>
@ -540,34 +539,26 @@ class ConnectionWizard extends App.WizardModal
@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)
@$('.js-progress progress').attr('value', job.result.sum)
@$('.js-progress progress').attr('max', job.result.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)
@tryResult(job)
else
@delay(@tryLoop, 4000)
)
tryResult: (job, total) =>
tryResult: (job) =>
@showSlide('js-try')
el = $(App.view('integration/exchange_summary')(job: job, countDone: total))
el = $(App.view('integration/exchange_summary')(job: job))
@el.find('.js-summary').html(el)
App.Config.set(

View file

@ -132,13 +132,12 @@ class Form extends App.Controller
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/ldap_last_import')(job: job, countDone: countDone))
el = $(App.view('integration/ldap_last_import')(job: job))
@lastImport.html(el)
activeDryRun: =>
@ -419,7 +418,7 @@ class ConnectionWizard extends App.WizardModal
if !_.isArray(user_attributes[key])
user_attributes[key] = [user_attributes[key]]
user_attributes_local =
"#{@wizardConfig['user_uid']}": 'login'
'samaccountname': 'login'
length = user_attributes.source.length-1
for count in [0..length]
if user_attributes.source[count] && user_attributes.dest[count]
@ -450,7 +449,7 @@ class ConnectionWizard extends App.WizardModal
buildRowsUserMap: (user_attribute_map) =>
# show static login row
userUidDisplayValue = @wizardConfig.wizardData.backend_user_attributes[ @wizardConfig['user_uid'] ]
userUidDisplayValue = @wizardConfig.wizardData.backend_user_attributes['samaccountname']
el = [
$(App.view('integration/ldap_user_attribute_row_read_only')(
@ -459,7 +458,7 @@ class ConnectionWizard extends App.WizardModal
))
]
for source, dest of user_attribute_map
continue if source == @wizardConfig['user_uid']
continue if source == 'samaccountname'
continue if !(source of @wizardConfig.wizardData.backend_user_attributes)
el.push @buildRowUserAttribute(source, dest)
el
@ -539,22 +538,12 @@ class ConnectionWizard extends App.WizardModal
@showAlert('js-error', (job.result.error || job.result.info))
return
if job.result && job.result.sum
if job.result && job.result.total
@$('.js-preprogress').addClass('hide')
@$('.js-analyzing').removeClass('hide')
total = 0
if job.result.created
total += job.result.created
if job.result.failed
total += job.result.failed
if job.result.skipped
total += job.result.skipped
if job.result.unchanged
total += job.result.unchanged
if job.result.updated
total += job.result.updated
@$('.js-progress progress').attr('value', total)
@$('.js-progress progress').attr('max', job.result.sum)
@$('.js-progress progress').attr('value', job.result.sum)
@$('.js-progress progress').attr('max', job.result.total)
if job.finished_at
# reset initial state in case the back button is used
@$('.js-preprogress').removeClass('hide')
@ -574,9 +563,8 @@ class ConnectionWizard extends App.WizardModal
for role_id, statistic of job.result.role_ids
role = App.Role.find(role_id)
job.result.roles[role.displayName()] = statistic
countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped
@showSlide('js-try')
el = $(App.view('integration/ldap_summary')(job: job, countDone: countDone))
el = $(App.view('integration/ldap_summary')(job: job))
@el.find('.js-summary').html(el)
App.Config.set(

View file

@ -32,7 +32,7 @@ class App.SettingsAreaItem extends App.Controller
)
new App.ControllerForm(
el: @el.find('.form-item'),
el: @el.find('.form-item')
model: { configure_attributes: @configure_attributes, className: '' }
autofocus: false
)

View file

@ -0,0 +1,21 @@
class App.SettingsAreaItemDefaultLocale extends App.SettingsAreaItem
render: =>
options = {}
locales = App.Locale.all()
for locale in locales
options[locale.locale] = locale.name
configure_attributes = [
{ name: 'locale_default', display: '', tag: 'searchable_select', null: false, class: 'input', options: options, default: @setting.state_current.value },
]
@html App.view(@template)(
setting: @setting
)
new App.ControllerForm(
el: @el.find('.form-item')
model: { configure_attributes: configure_attributes, className: '' }
autofocus: false
)

View file

@ -42,7 +42,7 @@ class App.SettingsAreaTicketNumber extends App.Controller
number = "#{App.Config.get('ticket_hook')}#{App.Config.get('system_id')}"
counter = '1'
if paramsItem.min_size
minSize = parseInt(paramsItem.min_size)
minSize = parseInt(paramsItem.min_size) - "#{App.Config.get('system_id')}".length
if paramsItem.checksum
minSize -= 1
if minSize > 1

View file

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

View file

@ -1,8 +1,7 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.richtext
@render: (attribute) ->
item = $( App.view('generic/richtext')( attribute: attribute ) )
@render: (attribute, params) ->
item = $( App.view('generic/richtext')(attribute: attribute) )
item.find('[contenteditable]').ce(
mode: attribute.type
maxlength: attribute.maxlength
@ -15,42 +14,42 @@ class App.UiElement.richtext
new App[plugin.controller](params)
if attribute.upload
item.append( $( App.view('generic/attachment')( attribute: attribute ) ) )
@attachments = []
item.append( $( App.view('generic/attachment')(attribute: attribute) ) )
renderAttachment = (file) =>
item.find('.attachments').append( App.view('generic/attachment_item')(
fileName: file.filename
fileSize: App.Utils.humanFileSize(file.size)
store_id: file.store_id
))
item.on(
'click'
"[data-id=#{file.store_id}]", (e) =>
@attachments = _.filter(
@attachments,
(item) ->
return if item.id isnt file.store_id
item
)
store_id = $(e.currentTarget).data('id')
renderFile = (file) =>
item.find('.attachments').append(App.view('generic/attachment_item')(file))
@attachments.push file
# delete attachment from storage
App.Ajax.request(
type: 'DELETE'
url: "#{App.Config.get('api_path')}/ticket_attachment_upload"
data: JSON.stringify(store_id: store_id),
processData: false
)
if params && params.attachments
for file in params.attachments
renderFile(file)
# remove attachment from dom
element = $(e.currentTarget).closest('.attachments')
$(e.currentTarget).closest('.attachment').remove()
# empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o
if element.find('.attachment').length == 0
element.empty()
# remove items
item.find('.attachments').on('click', '.js-delete', (e) =>
id = $(e.currentTarget).data('id')
@attachments = _.filter(
@attachments,
(item) ->
return if item.id.toString() is id.toString()
item
)
@attachments = []
# delete attachment from storage
App.Ajax.request(
type: 'DELETE'
url: "#{App.Config.get('api_path')}/ticket_attachment_upload"
data: JSON.stringify(id: id),
processData: false
)
# remove attachment from dom
element = $(e.currentTarget).closest('.attachments')
$(e.currentTarget).closest('.attachment').remove()
if element.find('.attachment').length == 0
element.empty()
)
@progressBar = item.find('.attachmentUpload-progressBar')
@progressText = item.find('.js-percentage')
@attachmentPlaceholder = item.find('.attachmentPlaceholder')
@ -84,7 +83,6 @@ class App.UiElement.richtext
# Called after received response from the server
onCompleted: (response) =>
response = JSON.parse(response)
@attachments.push response.data
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
@ -93,7 +91,7 @@ class App.UiElement.richtext
@progressBar.width(parseInt(0) + '%')
@progressText.text('')
renderAttachment(response.data)
renderFile(response.data)
item.find('input').val('')
App.Log.debug 'UiElement.richtext', 'upload complete', response.data
@ -111,4 +109,5 @@ class App.UiElement.richtext
)
)
App.Delay.set(u, 100, undefined, 'form_upload')
item

View file

@ -0,0 +1,4 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.richtext_search
@render: (attribute) ->
$( App.view('generic/input')( attribute: attribute ) )

View file

@ -0,0 +1,35 @@
# coffeelint: disable=camel_case_classes
class App.UiElement.select_search extends App.UiElement.ApplicationUiElement
@render: (attribute, params) ->
# set multiple option
if attribute.multiple
attribute.multiple = 'multiple'
else
attribute.multiple = ''
delete attribute.filter
# build options list based on config
@getConfigOptionList(attribute, params)
# build options list based on relation
@getRelationOptionList(attribute, params)
# add null selection if needed
@addNullOption(attribute, params)
# sort attribute.options
@sortOptions(attribute, params)
# finde selected/checked item of list
@selectedOptions(attribute, params)
# disable item of list
@disabledOptions(attribute, params)
# filter attributes
@filterOption(attribute, params)
# return item
$( App.view('generic/select')(attribute: attribute) )

View file

@ -22,9 +22,12 @@ class App.UiElement.ticket_selector
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
'boolean$': ['is', 'is not']
'integer$': ['is', 'is not']
'^radio$': ['is', 'is not']
'^select$': ['is', 'is not']
'^tree_select$': ['is', 'is not']
'^input$': ['contains', 'contains not']
'^richtext$': ['contains', 'contains not']
'^textarea$': ['contains', 'contains not']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']
@ -34,9 +37,12 @@ class App.UiElement.ticket_selector
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed']
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed']
'boolean$': ['is', 'is not', 'has changed']
'integer$': ['is', 'is not', 'has changed']
'^radio$': ['is', 'is not', 'has changed']
'^select$': ['is', 'is not', 'has changed']
'^tree_select$': ['is', 'is not', 'has changed']
'^input$': ['contains', 'contains not', 'has changed']
'^richtext$': ['contains', 'contains not', 'has changed']
'^textarea$': ['contains', 'contains not', 'has changed']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']

View file

@ -14,6 +14,8 @@ class App.TicketCreate extends App.Controller
# define default type
@default_type = 'phone-in'
@formId = App.ControllerForm.formId()
# remember split info if exists
@split = ''
if @ticket_id && @article_id
@ -92,6 +94,10 @@ class App.TicketCreate extends App.Controller
else
@$('[name="cc"]').closest('.form-group').addClass('hide')
# show notice
@$('.js-note').addClass('hide')
@$(".js-note[data-type='#{type}']").removeClass('hide')
App.TaskManager.touch(@task_key)
meta: =>
@ -158,7 +164,7 @@ class App.TicketCreate extends App.Controller
# get data / in case also ticket data for split
buildScreen: (params) =>
if !params.ticket_id && !params.article_id
if _.isEmpty(params.ticket_id) && _.isEmpty(params.article_id)
if !_.isEmpty(params.customer_id)
@render(options: { customer_id: params.customer_id })
return
@ -173,6 +179,7 @@ class App.TicketCreate extends App.Controller
data:
ticket_id: params.ticket_id
article_id: params.article_id
form_id: @formId
processData: true
success: (data, status, xhr) =>
@ -194,6 +201,9 @@ class App.TicketCreate extends App.Controller
else
t.body = App.Utils.text2html(a.body)
# add attachments
t.attachments = data.attachments
# render page
@render(options: t)
)
@ -201,23 +211,20 @@ class App.TicketCreate extends App.Controller
render: (template = {}) ->
# get params
params = {}
params = @prefilledParams || {}
if template && !_.isEmpty(template.options)
params = template.options
else if App.TaskManager.get(@task_key) && !_.isEmpty(App.TaskManager.get(@task_key).state)
params = App.TaskManager.get(@task_key).state
if !_.isEmpty(params['form_id'])
@formId = params['form_id']
if params['form_id']
@form_id = params['form_id']
else
@form_id = App.ControllerForm.formId()
@html App.view('agent_ticket_create')(
@html(App.view('agent_ticket_create')(
head: 'New Ticket'
agent: @permissionCheck('ticket.agent')
admin: @permissionCheck('admin')
form_id: @form_id
)
form_id: @formId
))
signatureChanges = (params, attribute, attributes, classname, form, ui) =>
if attribute && attribute.name is 'group_id'
@ -272,7 +279,7 @@ class App.TicketCreate extends App.Controller
}
new App.ControllerForm(
el: @$('.ticket-form-top')
form_id: @form_id
form_id: @formId
model: App.Ticket
screen: 'create_top'
events:
@ -288,14 +295,14 @@ class App.TicketCreate extends App.Controller
new App.ControllerForm(
el: @$('.article-form-top')
form_id: @form_id
form_id: @formId
model: App.TicketArticle
screen: 'create_top'
params: params
)
new App.ControllerForm(
el: @$('.ticket-form-middle')
form_id: @form_id
form_id: @formId
model: App.Ticket
screen: 'create_middle'
events:
@ -310,7 +317,7 @@ class App.TicketCreate extends App.Controller
)
new App.ControllerForm(
el: @$('.ticket-form-bottom')
form_id: @form_id
form_id: @formId
model: App.Ticket
screen: 'create_bottom'
events:
@ -420,7 +427,7 @@ class App.TicketCreate extends App.Controller
body: params.body
type_id: type.id
sender_id: sender.id
form_id: @form_id
form_id: @formId
content_type: 'text/html'
}
else
@ -432,7 +439,7 @@ class App.TicketCreate extends App.Controller
body: params.body
type_id: type.id
sender_id: sender.id
form_id: @form_id
form_id: @formId
content_type: 'text/html'
}

View file

@ -32,9 +32,7 @@ class App.TicketCreateSidebar extends App.Controller
params: @params
query: @query
)
item = @sidebarBackends[key].sidebarItem()
if item
@sidebarItems.push item
@sidebarItems.push @sidebarBackends[key]
new App.Sidebar(
el: @el

View file

@ -1,26 +1,62 @@
class SidebarCustomer extends App.Controller
sidebarItem: =>
return if !@permissionCheck('ticket.agent')
return if !@params.customer_id
{
head: 'Customer'
name: 'customer'
icon: 'person'
actions: [
return if _.isEmpty(@params.customer_id)
@item = {
name: 'customer'
badgeCallback: @badgeRender
sidebarHead: 'Customer'
sidebarCallback: @showCustomer
sidebarActions: [
{
title: 'Edit Customer'
name: 'customer-edit'
callback: @editCustomer
},
]
callback: @showCustomer
}
metaBadge: (user) =>
counter = ''
cssClass = ''
counter = @sidebarItemCounter(user)
if @Config.get('ui_sidebar_open_ticket_indicator_colored') is true
if counter == 1
cssClass = 'tabsSidebar-tab-count--warning'
if counter > 1
cssClass = 'tabsSidebar-tab-count--danger'
{
name: 'customer'
icon: 'person'
counterPossible: true
counter: counter
cssClass: cssClass
}
badgeRender: (el) =>
@badgeEl = el
if App.User.exists(@params.customer_id)
user = App.User.find(@params.customer_id)
@badgeRenderLocal(user)
badgeRenderLocal: (user) =>
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge(user)))
sidebarItemCounter: (user) ->
counter = ''
if user && user.preferences && user.preferences.tickets_open
counter = user.preferences.tickets_open
counter
showCustomer: (el) =>
@el = el
@elSidebar = el
return if _.isEmpty(@params.customer_id)
new App.WidgetUser(
el: @el
el: @elSidebar
user_id: @params.customer_id
callback: @badgeRenderLocal
)
editCustomer: =>
@ -32,7 +68,7 @@ class SidebarCustomer extends App.Controller
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
container: @elSidebar.closest('.content')
)
App.Config.set('200-Customer', SidebarCustomer, 'TicketCreateSidebar')

View file

@ -6,24 +6,25 @@ class SidebarOrganization extends App.Controller
customer = App.User.find(@params.customer_id)
@organization_id = customer.organization_id
return if !@organization_id
{
head: 'Organization'
@item = {
name: 'organization'
icon: 'group'
actions: [
badgeIcon: 'group'
sidebarHead: 'Organization'
sidebarCallback: @showOrganization
sidebarActions: [
{
title: 'Edit Organization'
name: 'organization-edit'
callback: @editOrganization
},
]
callback: @showOrganization
}
@item
showOrganization: (el) =>
@el = el
@elSidebar = el
new App.WidgetOrganization(
el: @el
el: @elSidebar
organization_id: @organization_id
)
@ -35,7 +36,7 @@ class SidebarOrganization extends App.Controller
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
container: @elSidebar.closest('.content')
)
App.Config.set('300-Organization', SidebarOrganization, 'TicketCreateSidebar')

View file

@ -1,13 +1,15 @@
class SidebarTemplate extends App.Controller
sidebarItem: =>
return if !@permissionCheck('ticket.agent')
{
head: 'Templates'
name: 'template'
icon: 'templates'
actions: []
callback: @showTemplates
@item = {
name: 'template'
badgeIcon: 'templates'
badgeCallback: @badgeRender
sidebarHead: 'Templates'
sidebarActions: []
sidebarCallback: @showTemplates
}
@item
showTemplates: (el) =>
@el = el

View file

@ -35,7 +35,7 @@ class App.CustomerChat extends App.Controller
active_agent_ids: []
@render()
@on 'layout-has-changed', @propagateLayoutChange
@on('layout-has-changed', @propagateLayoutChange)
# update navbar on new status
@bind('chat_status_agent', (data) =>
@ -163,6 +163,12 @@ class App.CustomerChat extends App.Controller
@title 'Customer Chat', true
@navupdate '#customer_chat'
if params.session_id
callback = (session) =>
@addChat(session)
App.ChatSession.full(params.session_id, callback)
@navigate '#customer_chat'
active: (state) =>
return @shown if state is undefined
@shown = state
@ -264,10 +270,11 @@ class App.CustomerChat extends App.Controller
addChat: (session) ->
return if @chatWindows[session.session_id]
chat = new ChatWindow
chat = new ChatWindow(
session: session
removeCallback: @removeChat
messageCallback: @updateNavMenu
)
@workspace.append chat.el
chat.render()
@ -289,7 +296,7 @@ class App.CustomerChat extends App.Controller
propagateLayoutChange: (event) =>
# adjust scroll position on layoutChange
for session_id, chat of @chatWindows
chat.trigger 'layout-changed'
chat.trigger('layout-changed')
acceptChat: =>
return if @windowCount() >= @maxChatWindows
@ -324,19 +331,6 @@ class App.CustomerChat extends App.Controller
currentPosition: =>
@$('.main').scrollTop()
class CustomerChatRouter extends App.ControllerPermanent
requiredPermission: 'chat.agent'
constructor: (params) ->
super
App.TaskManager.execute(
key: 'CustomerChat'
controller: 'CustomerChat'
params: {}
show: true
persistent: true
)
class ChatWindow extends App.Controller
className: 'chat-window'
@ -348,6 +342,9 @@ class ChatWindow extends App.Controller
'click .js-close': 'close'
'click .js-disconnect': 'disconnect'
'click .js-scrollHint': 'onScrollHintClick'
'click .js-info': 'toggleMeta'
'click .js-createTicket': 'ticketCreate'
'submit .js-metaForm': 'sendMetaForm'
elements:
'.js-customerChatInput': 'input'
@ -355,8 +352,11 @@ class ChatWindow extends App.Controller
'.js-close': 'closeButton'
'.js-disconnect': 'disconnectButton'
'.js-body': 'body'
'.js-meta': 'meta'
'.js-name': 'metaName'
'.js-scrollHolder': 'scrollHolder'
'.js-scrollHint': 'scrollHint'
'.js-metaForm': 'metaForm'
sounds:
message: new Audio('assets/sounds/chat_message.mp3')
@ -374,9 +374,11 @@ class ChatWindow extends App.Controller
@scrollSnapTolerance = 10 # pixels
@chat = App.Chat.find(@session.chat_id)
@name = "#{@chat.displayName()} ##{@session.id}"
@name = @chat.displayName()
if @session && !_.isEmpty(@session.name)
@name = @session.name
@on 'layout-change', @onLayoutChange
@on('layout-change', @onLayoutChange)
@bind('chat_session_typing', (data) =>
return if data.session_id isnt @session.session_id
@ -413,12 +415,45 @@ class ChatWindow extends App.Controller
onLayoutChange: =>
@scrollToBottom()
render: ->
@html App.view('customer_chat/chat_window')
name: @name
toggleMeta: =>
if @meta.hasClass('hidden')
@showMeta()
else
@hideMeta()
@el.one 'transitionend', @onTransitionend
@scrollHolder.scroll @detectScrolledtoBottom
hideMeta: =>
@body.removeClass('hidden')
@meta.addClass('hidden')
@sendMetaForm()
showMeta: =>
@body.addClass('hidden')
@meta.removeClass('hidden')
sendMetaForm: (e) =>
if e
e.preventDefault()
params = @formParam(@metaForm)
App.WebSocket.send(
event:'chat_session_update'
data:
session_id: @session.session_id
name: params.name
tags: params.tags
)
if !_.isEmpty(params.name)
@metaName.text(params.name)
render: ->
@html App.view('customer_chat/chat_window')(
name: @name
session: @session
)
@el.one('transitionend', @onTransitionend)
@scrollHolder.scroll(@detectScrolledtoBottom)
# force repaint
@el.prop('offsetHeight')
@ -426,18 +461,24 @@ class ChatWindow extends App.Controller
# @addMessage 'Hello. My name is Roger, how can I help you?', 'agent'
if @session
# set chat to offline if state is already closed
activeChat = true
if @session.state is 'closed'
activeChat = false
if @session && @session.preferences && @session.preferences.url
@addNoticeMessage(@session.preferences.url)
@addNoticeMessage(@session.preferences.url, undefined, activeChat)
if @session.messages
for message in @session.messages
if message.created_by_id
@addMessage message.content, 'agent'
@addMessage(message.content, 'agent', false, activeChat)
else
@addMessage message.content, 'customer'
@addMessage(message.content, 'customer', false, activeChat)
# send init reply
if !@session.messages || _.isEmpty(@session.messages)
if activeChat && _.isEmpty(@session.messages)
preferences = @Session.get('preferences')
if preferences.chat && preferences.chat.phrase
phrases = preferences.chat.phrase[@session.chat_id]
@ -447,20 +488,9 @@ class ChatWindow extends App.Controller
@input.html(phrase)
@sendMessage(1600)
@$('.js-info').popover(
trigger: 'hover'
html: true
animation: false
delay: 0
placement: 'bottom'
container: 'body' # place in body do prevent it from animating
title: ->
App.i18n.translateContent('Details')
content: =>
App.view('customer_chat/chat_window_info')(
session: @session
)
)
# set chat to offline if state is already closed
if !activeChat
@goOffline()
# show text module UI
new App.WidgetTextModule(
@ -470,6 +500,18 @@ class ChatWindow extends App.Controller
config: App.Config.all()
)
configureAttributesOutbound = [
{ name: 'name', display: 'Name', tag: 'input', null: true, },
{ name: 'tags', display: 'Tags', tag: 'tag', null: true, },
]
new App.ControllerForm(
el: @$('.js-metaForm')
model:
configure_attributes: configureAttributesOutbound
className: ''
params: @session
)
focus: =>
@input.focus()
@ -498,7 +540,8 @@ class ChatWindow extends App.Controller
@goOffline()
close: =>
@el.one 'transitionend', { callback: @release }, @onTransitionend
@sendMetaForm()
@el.one('transitionend', { callback: @release }, @onTransitionend)
@el.removeClass('is-open')
if @removeCallback
@removeCallback(@session.session_id)
@ -577,7 +620,8 @@ class ChatWindow extends App.Controller
)
@delay(send, delay)
@addMessage content, 'agent'
@hideMeta()
@addMessage(content, 'agent')
@input.html('')
updateModified: (state) =>
@ -614,18 +658,19 @@ class ChatWindow extends App.Controller
@messageCallback(@session.session_id)
@unreadMessagesCounter = 0
addMessage: (message, sender, isNew) =>
@maybeAddTimestamp()
addMessage: (message, sender, isNew, useMaybeAddTimestamp = true) =>
@maybeAddTimestamp() if useMaybeAddTimestamp
@lastAddedType = sender
@body.append App.view('customer_chat/chat_message')
@body.append App.view('customer_chat/chat_message')(
message: message
sender: sender
isNew: isNew
timestamp: Date.now()
)
@scrollToBottom showHint: true
@scrollToBottom(showHint: true)
showWritingLoader: =>
if !@isTyping
@ -667,33 +712,37 @@ class ChatWindow extends App.Controller
@lastAddedType = 'timestamp'
addTimestamp: (label, time) =>
@body.append App.view('customer_chat/chat_timestamp')
@body.append App.view('customer_chat/chat_timestamp')(
label: label
time: time
)
updateLastTimestamp: (label, time) ->
@body
.find('.js-timestamp')
.last()
.replaceWith App.view('customer_chat/chat_timestamp')
.replaceWith App.view('customer_chat/chat_timestamp')(
label: label
time: time
)
addStatusMessage: (message, args) ->
@maybeAddTimestamp()
addStatusMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_status_message')
@body.append App.view('customer_chat/chat_status_message')(
message: message
args: args
)
@scrollToBottom()
addNoticeMessage: (message, args) ->
@maybeAddTimestamp()
addNoticeMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_notice_message')
@body.append App.view('customer_chat/chat_notice_message')(
message: message
args: args
)
@scrollToBottom()
@ -717,6 +766,37 @@ class ChatWindow extends App.Controller
else if showHint
@showScrollHint()
ticketCreate: (e) =>
e.preventDefault()
id = Math.floor( Math.random() * 99999 )
@navigate "#ticket/create/id/#{id}"
# cleanup params
fqdn = App.Config.get('fqdn')
http_type = App.Config.get('http_type')
url = ''
session = @session
# in case we do not have a model, create one
if session && !session.uiUrl
session = new App.ChatSession(session)
if session && session.uiUrl
url = session.uiUrl()
clean_params =
id: id
prefilledParams:
body: "#{http_type}://#{fqdn}/#{url}"
title: 'Chat'
App.TaskManager.execute(
key: "TicketCreateScreen-#{id}"
controller: 'TicketCreate'
params: clean_params
show: true
)
class Setting extends App.ControllerModal
buttonClose: true
buttonCancel: true
@ -784,6 +864,24 @@ class Setting extends App.ControllerModal
msg: App.i18n.translateContent(data.message)
)
class CustomerChatRouter extends App.ControllerPermanent
requiredPermission: 'chat.agent'
constructor: (params) ->
super
# cleanup params
clean_params =
session_id: params.session_id
App.TaskManager.execute(
key: 'CustomerChat'
controller: 'CustomerChat'
params: clean_params
show: true
persistent: true
)
App.Config.set('customer_chat', CustomerChatRouter, 'Routes')
App.Config.set('customer_chat/session/:session_id', CustomerChatRouter, 'Routes')
App.Config.set('CustomerChat', { controller: 'CustomerChat', permission: ['chat.agent'] }, 'permanentTask')
App.Config.set('CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', shown: false, permission: ['chat.agent'], class: 'chat' }, 'NavBar')

View file

@ -337,11 +337,9 @@ class Base extends App.WizardFullScreen
@hideAlerts()
@disable(e)
# get params
@params = @formParam(e.target)
# add logo
@params.logo = @logoPreview.attr('src')
@params.locale_default = App.i18n.detectBrowserLocale()
store = (logoResizeDataUrl) =>
@params.logo_resize = logoResizeDataUrl
@ -354,7 +352,7 @@ class Base extends App.WizardFullScreen
success: (data, status, xhr) =>
if data.result is 'ok'
for key, value of data.settings
App.Config.set( key, value )
App.Config.set(key, value)
if App.Config.get('system_online_service')
@navigate 'getting_started/channel/email_pre_configured'
else

View file

@ -3,6 +3,7 @@ class App.IdoitObjectSelector extends App.ControllerModal
buttonCancel: true
buttonSubmit: true
head: 'i-doit'
lastSearchTermEmpty: false
content: ->
@ajax(
@ -44,16 +45,24 @@ class App.IdoitObjectSelector extends App.ControllerModal
''
search: (filter) =>
if _.isEmpty(filter.type) && _.isEmpty(filter.title)
@lastSearchTermEmpty = true
@renderResult()
return
if _.isEmpty(filter.type)
delete filter.type
if _.isEmpty(filter.title)
delete filter.title
else
filter.title = "%#{filter.title}%"
@lastSearchTermEmpty = false
@ajax(
id: 'idoit-object-selector'
type: 'POST'
url: "#{@apiPath}/integration/idoit"
data: JSON.stringify(method: 'cmdb.objects', filter: filter)
success: (data, status, xhr) =>
return if @lastSearchTermEmpty
@renderResult(data.response.result)
error: (xhr, status, error) =>

View file

@ -154,35 +154,35 @@ class Index extends App.ControllerContent
processData: true
success: (data, status, xhr) =>
if data.result is 'import_done'
window.location.reload()
return
if data.result is 'error'
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent(data.message))
else
@$('.js-error').addClass('hide')
if data.message is 'not running' && @updateMigrationDisplayLoop > 16
if _.isEmpty(data.result) && @updateMigrationDisplayLoop > 16
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
return
if data.result is 'in_progress'
for key, item of data.data
if item.done > item.total
item.done = item.total
if !_.isEmpty(data.result['error'])
@$('.js-error').removeClass('hide')
@$('.js-error').html(App.i18n.translateContent(data.result['error']))
else
@$('.js-error').addClass('hide')
if key == 'Ticket' && item.total >= 1000
if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
window.location.reload()
return
if !_.isEmpty(data.result)
for model, stats of data.result
if stats.sum > stats.total
stats.sum = stats.total
if model == 'Ticket' && stats.total >= 1000
@ticketCountInfo.removeClass('hide')
element = @$('.js-' + key.toLowerCase() )
element.find('.js-done').text(item.done)
element.find('.js-total').text(item.total)
element.find('progress').attr('max', item.total )
element.find('progress').attr('value', item.done )
if item.total <= item.done
element = @$('.js-' + model.toLowerCase() )
element.find('.js-done').text(stats.sum)
element.find('.js-total').text(stats.total)
element.find('progress').attr('max', stats.total )
element.find('progress').attr('value', stats.sum )
if stats.total <= stats.sum
element.addClass('is-done')
else
element.removeClass('is-done')

View file

@ -349,7 +349,7 @@ class LayoutRefCommunicationReply extends App.ControllerContent
file = @uploadQueue.shift()
# console.log "working of", file, "from", @uploadQueue
@fakeUpload file.name, file.size, @workOfUploadQueue
@fakeUpload(file.name, file.size, @workOfUploadQueue)
humanFileSize: (size) ->
i = Math.floor( Math.log(size) / Math.log(1024) )
@ -363,27 +363,27 @@ class LayoutRefCommunicationReply extends App.ControllerContent
@attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide')
fakeUpload: (fileName, fileSize, callback) ->
fakeUpload: (filename, size, callback) ->
@attachmentPlaceholder.addClass('hide')
@attachmentUpload.removeClass('hide')
progress = 0
duration = fileSize / 1024
duration = size / 1024
for i in [0..100]
setTimeout @updateUploadProgress, i*duration/100 , i
setTimeout (=>
callback()
@renderAttachment(fileName, fileSize)
@renderAttachment(filename, size)
), duration
renderAttachment: (fileName, fileSize) =>
@attachments.push([fileName, fileSize])
@attachmentsHolder.append App.view('generic/attachment_item')
fileName: fileName
fileSize: @humanFileSize(fileSize)
renderAttachment: (filename, size) =>
@attachments.push([filename, size])
@attachmentsHolder.append(App.view('generic/attachment_item')
filename: filename
size: @humanFileSize(size)
)
App.Config.set( 'layout_ref/communication_reply/:content', LayoutRefCommunicationReply, 'Routes' )
@ -2121,7 +2121,7 @@ class TwitterConversationRef extends App.ControllerContent
open: 88
closed: 20
maxTextLength: 140
maxTextLength: 280
warningTextLength: 10
constructor: ->

View file

@ -23,17 +23,22 @@ class Index extends App.ControllerSubContent
]
container: @el.closest('.content')
large: true
dndCallback: =>
dndCallback: (e, item) =>
items = @el.find('table > tbody > tr')
order = []
prios = []
prio = 0
for item in items
prio += 1
id = $(item).data('id')
overview = App.Overview.find(id)
if overview.prio isnt prio
overview.prio = prio
overview.save()
prios.push [id, prio]
@ajax(
id: 'overview_prio'
type: 'POST'
url: "#{@apiPath}/overviews_prio"
processData: true
data: JSON.stringify(prios: prios)
)
)
App.Config.set('Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, permission: ['admin.overview'] }, 'NavBarAdmin')

View file

@ -108,33 +108,40 @@ class Graph extends App.ControllerContent
@render()
render: =>
update: (data) =>
update = (data) =>
# show only selected lines
dataNew = {}
for key, value of data.data
if @params.backendSelected[key] is true
dataNew[key] = value
@ui.storeParams()
# show only selected lines
dataNew = {}
for key, value of data.data
if @params.backendSelected[key] is true
dataNew[key] = value
@ui.storeParams()
if !@lastNewData
@lastNewData = {}
if !@lastNewData
@lastNewData = {}
return if @lastNewData && JSON.stringify(dataNew) is JSON.stringify(@lastNewData)
@lastNewData = dataNew
return if @lastNewData && JSON.stringify(dataNew) is JSON.stringify(@lastNewData)
@lastNewData = dataNew
@draw(dataNew)
t = new Date
@el.find('#download-chart').html(t.toString())
new Download(
@draw(dataNew)
t = new Date
@el.find('#download-chart').html(t.toString())
if @downloadWidget
@downloadWidget.update(
config: @config
params: @params
ui: @ui
)
else
@downloadWidget = new Download(
el: @el.find('.js-dataDownload')
config: @config
params: @params
ui: @ui
)
render: =>
url = "#{@apiPath}/reports/generate"
interval = 5 * 60000
if @params.timeRange is 'year'
@ -142,9 +149,9 @@ class Graph extends App.ControllerContent
if @params.timeRange is 'month'
interval = 60000
if @params.timeRange is 'week'
interval = 40000
interval = 50000
if @params.timeRange is 'day'
interval = 20000
interval = 30000
if @params.timeRange is 'realtime'
interval = 10000
@ -164,7 +171,7 @@ class Graph extends App.ControllerContent
)
processData: true
success: (data) =>
update(data)
@update(data)
@delay(@render, interval, 'report-update', 'page')
)
@ -215,7 +222,7 @@ class Graph extends App.ControllerContent
class Download extends App.Controller
events:
'click .js-dataDownloadBackendSelector': 'tableUpdate'
'click .js-dataDownloadBackendSelector': 'selectBackend'
constructor: (data) ->
@ -225,7 +232,24 @@ class Download extends App.Controller
super
@render()
render: ->
selectBackend: (e) =>
e.preventDefault()
@el.find('.js-dataDownloadBackendSelector').parent().removeClass('active')
$(e.target).parent().addClass('active')
@profileSelectedId = $(e.target).data('profile-id')
@params.downloadBackendSelected = $(e.target).data('backend')
@ui.storeParams()
@table = false
@render()
update: =>
@render()
render: =>
if !@contentRendered
@contentRendered = true
@html(App.view('report/download_content')())
reports = []
@ -244,44 +268,84 @@ class Download extends App.Controller
@profileSelectedId = key
profiles.push App.ReportProfile.find(key)
@html App.view('report/download_header')(
downloadHeaderHtml = App.view('report/download_header')(
reports: reports
profiles: profiles
downloadBackendSelected: @params.downloadBackendSelected
metric: @config.metric[@params.metric]
)
if downloadHeaderHtml isnt @downloadHeaderHtml
@el.find('.js-dataDownloadHeader').html(downloadHeaderHtml)
@downloadHeaderHtml = downloadHeaderHtml
@tableUpdate()
tableUpdate: (e) =>
if e
e.preventDefault()
@el.find('.js-dataDownloadBackendSelector').parent().removeClass('active')
$(e.target).parent().addClass('active')
@profileSelectedId = $(e.target).data('profile-id')
@params.downloadBackendSelected = $(e.target).data('backend')
@ui.storeParams()
tableRender: (tickets, count) =>
if _.isEmpty(tickets)
@$('.js-dataDownloadButton').html('')
@$('.js-dataDownloadTable').html('')
return
table = (tickets, count) =>
url = '#ticket/zoom/'
if App.Config.get('import_mode')
url = App.Config.get('import_otrs_endpoint') + '/index.pl?Action=AgentTicketZoom;TicketID='
if _.isEmpty(tickets)
@el.find('.js-dataDownloadTable').html('')
else
profile_id = 0
for key, value of @params.profileSelected
if value
profile_id = key
downloadUrl = "#{@apiPath}/reports/sets?sheet=true;metric=#{@params.metric};year=#{@params.year};month=#{@params.month};week=#{@params.week};day=#{@params.day};timeRange=#{@params.timeRange};profile_id=#{profile_id};downloadBackendSelected=#{@params.downloadBackendSelected}"
html = App.view('report/download_list')(
tickets: tickets
count: count
url: url
download: downloadUrl
)
@el.find('.js-dataDownloadTable').html(html)
profile_id = 0
for key, value of @params.profileSelected
if value
profile_id = key
downloadUrl = "#{@apiPath}/reports/sets?sheet=true;metric=#{@params.metric};year=#{@params.year};month=#{@params.month};week=#{@params.week};day=#{@params.day};timeRange=#{@params.timeRange};profile_id=#{profile_id};downloadBackendSelected=#{@params.downloadBackendSelected}"
@$('.js-dataDownloadButton').html(App.view('report/download_button')(
count: count
downloadUrl: downloadUrl
))
openTicket = (id,e) =>
ticket = App.Ticket.findNative(id)
@navigate ticket.uiUrl()
callbackTicketTitleAdd = (value, object, attribute, attributes) ->
attribute.title = object.title
value
callbackLinkToTicket = (value, object, attribute, attributes) ->
attribute.link = object.uiUrl()
value
callbackIconHeader = (headers) ->
attribute =
name: 'icon'
display: ''
translation: false
width: '28px'
displayWidth:28
unresizable: true
headers.unshift(0)
headers[0] = attribute
headers
callbackIcon = (value, object, attribute, header) ->
value = ' '
attribute.class = object.iconClass()
attribute.link = ''
attribute.title = object.iconTitle()
value
params =
el: @el.find('.js-dataDownloadTable')
model: App.Ticket
objects: tickets
overviewAttributes: ['number', 'title', 'state', 'group', 'created_at']
bindRow:
events:
'click': openTicket
callbackHeader: [ callbackIconHeader ]
callbackAttributes:
icon:
[ callbackIcon ]
title:
[ callbackLinkToTicket, callbackTicketTitleAdd ]
number:
[ callbackLinkToTicket, callbackTicketTitleAdd ]
if !@table
@table = new App.ControllerTable(params)
else
@table.update(objects: tickets)
tableUpdate: =>
@ajax(
id: 'report_download'
type: 'POST'
@ -298,15 +362,14 @@ class Download extends App.Controller
downloadBackendSelected: @params.downloadBackendSelected
)
processData: true
success: (data) ->
success: (data) =>
App.Collection.loadAssets(data.assets)
ticket_collection = []
if data.ticket_ids
for record_id in data.ticket_ids
ticket = App.Ticket.fullLocal( record_id )
ticket = App.Ticket.fullLocal(record_id)
ticket_collection.push ticket
table(ticket_collection, data.count)
@tableRender(ticket_collection, data.count)
)
class TimeRangePicker extends App.Controller

View file

@ -79,6 +79,7 @@ class App.Search extends App.Controller
@tabs = []
for model in App.Config.get('models_searchable')
model = model.replace(/::/, '')
tab =
name: model
model: model

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, disableCreateObject: true },
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: false },
]
controller = new App.ControllerForm(
model:
@ -18,8 +18,19 @@ class App.TicketCustomer extends App.ControllerModal
onSubmit: (e) =>
params = @formParam(e.target)
@customer_id = params['customer_id']
ticket = App.Ticket.find(@ticket_id)
ticket.customer_id = params['customer_id']
errors = ticket.validate()
if !_.isEmpty(errors)
@log 'error', errors
@formValidate(
form: e.target
errors: errors
)
return
@customer_id = params['customer_id']
callback = =>
# close modal

View file

@ -956,7 +956,6 @@ class Table extends App.Controller
ticketListShow = []
for ticket in tickets
ticketListShow.push App.Ticket.find(ticket.id)
console.log('overview', overview)
@overview = App.Overview.find(overview.id)
@table.update(
overviewAttributes: @overview.view.s

View file

@ -461,6 +461,7 @@ class App.TicketZoom extends App.Controller
ui: @
highligher: @highligher
ticket_article_ids: @ticket_article_ids
form_id: @form_id
)
new App.TicketCustomerAvatar(

View file

@ -0,0 +1,34 @@
class Delete
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
if article.type.name is 'note'
user = undefined
if App.Session.get('id') == article.created_by_id
user = App.User.find(App.Session.get('id'))
if user.permission('ticket.agent')
actions.push {
name: 'delete'
type: 'delete'
icon: 'trash'
href: '#'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'delete'
callback = ->
article = App.TicketArticle.find(article.id)
article.destroy()
new App.ControllerConfirm(
message: 'Sure?'
callback: callback
container: ui.el.closest('.content')
)
true
App.Config.set('900-Delete', Delete, 'TicketZoomArticleAction')

View file

@ -0,0 +1,203 @@
class EmailReply extends App.Controller
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
group = ticket.group
if group.email_address_id && (article.type.name is 'email' || article.type.name is 'web')
actions.push {
name: 'reply'
type: 'emailReply'
icon: 'reply'
href: '#'
}
recipients = []
if article.sender.name is 'Customer'
if article.from
localRecipients = emailAddresses.parseAddressList(article.from)
if localRecipients
recipients = recipients.concat localRecipients
if article.to
localRecipients = emailAddresses.parseAddressList(article.to)
if localRecipients
recipients = recipients.concat localRecipients
if article.cc
localRecipients = emailAddresses.parseAddressList(article.cc)
if localRecipients
recipients = recipients.concat localRecipients
# remove system addresses
localAddresses = App.EmailAddress.all()
forgeinRecipients = []
recipientUsed = {}
for recipient in recipients
if !_.isEmpty(recipient.address)
localRecipientAddress = recipient.address.toString().toLowerCase()
if !recipientUsed[localRecipientAddress]
recipientUsed[localRecipientAddress] = true
localAddress = false
for address in localAddresses
if localRecipientAddress is address.email.toString().toLowerCase()
recipientUsed[localRecipientAddress] = true
localAddress = true
if !localAddress
forgeinRecipients.push recipient
# check if reply all is neede
if forgeinRecipients.length > 1
actions.push {
name: 'reply all'
type: 'emailReplyAll'
icon: 'reply-all'
href: '#'
}
actions.push {
name: 'forward'
type: 'emailForward'
icon: 'forward'
href: '#'
}
if article.sender.name is 'Customer' && article.type.name is 'phone'
actions.push {
name: 'reply'
type: 'emailReply'
icon: 'reply'
href: '#'
}
actions.push {
name: 'forward'
type: 'emailForward'
icon: 'forward'
href: '#'
}
if article.sender.name is 'Agent' && article.type.name is 'phone'
actions.push {
name: 'reply'
type: 'emailReply'
icon: 'reply'
href: '#'
}
actions.push {
name: 'forward'
type: 'emailForward'
icon: 'forward'
href: '#'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'emailReply' && type isnt 'emailReplyAll' && type isnt 'emailForward'
if type is 'emailReply'
@emailReply(false, ticket, article, ui)
else if type is 'emailReplyAll'
@emailReply(true, ticket, article, ui)
else if type is 'emailForward'
@emailForward(ticket, article, ui)
true
@emailReply: (all = false, ticket, article, ui) ->
# get reference article
type = App.TicketArticleType.find(article.type_id)
article_created_by = App.User.find(article.created_by_id)
email_addresses = App.EmailAddress.all()
ui.scrollToCompose()
# empty form
articleNew = App.Utils.getRecipientArticle(ticket, article, article_created_by, type, email_addresses, all)
# get current body
body = ui.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()
if !selected
selected = App.ClipBoard.getSelected('text')
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>"
# add selected text to body
body = selected + body
articleNew.body = body
type = App.TicketArticleType.findByAttribute(name:'email')
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
signaturePosition: signaturePosition
})
true
@emailForward: (ticket, article, ui) ->
ui.scrollToCompose()
signaturePosition = 'top'
body = ''
if article.content_type.match('html')
body = App.Utils.textCleanup(article.body)
if article.content_type.match('plain')
body = App.Utils.textCleanup(article.body)
body = App.Utils.text2html(body)
body = "<br/><div>---Begin forwarded message:---<br/><br/></div><div><blockquote type=\"cite\">#{body}</blockquote></div><div><br></div>"
articleNew = {}
articleNew.body = body
type = App.TicketArticleType.findByAttribute(name:'email')
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
signaturePosition: signaturePosition
})
# add attachments to form
App.Ajax.request(
id: "ticket_attachment_clone#{ui.form_id}"
type: 'POST'
url: "#{App.Config.get('api_path')}/ticket_attachment_upload_clone_by_article/#{article.id}"
data: JSON.stringify(form_id: ui.form_id)
processData: true
success: (data, status, xhr) ->
return if _.isEmpty(data.attachments)
App.Event.trigger('ui::ticket::addArticleAttachent', {
ticket: ticket
article: article
attachments: data.attachments
form_id: ui.form_id
})
)
true
App.Config.set('200-EmailReply', EmailReply, 'TicketZoomArticleAction')

View file

@ -0,0 +1,37 @@
class FacebookReply
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
if article.type.name is 'facebook feed post' || article.type.name is 'facebook feed comment'
actions.push {
name: 'reply'
type: 'facebookFeedReply'
icon: 'reply'
href: '#'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'facebookFeedReply'
ui.scrollToCompose()
type = App.TicketArticleType.findByAttribute('name', 'facebook feed comment')
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
})
true
App.Config.set('300-FacebookReply', FacebookReply, 'TicketZoomArticleAction')

View file

@ -0,0 +1,40 @@
class Internal
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
if article.internal is true
actions.push {
name: 'set to public'
type: 'public'
icon: 'lock-open'
}
else
actions.push {
name: 'set to internal'
type: 'internal'
icon: 'lock'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'internal' && type isnt 'public'
# storage update
internal = true
if article.internal == true
internal = false
ui.lastAttributres.internal = internal
article.updateAttributes(internal: internal)
# runtime update
if internal
articleContainer.addClass('is-internal')
else
articleContainer.removeClass('is-internal')
ui.render()
true
App.Config.set('100-Internal', Internal, 'TicketZoomArticleAction')

View file

@ -0,0 +1,18 @@
class Split
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
actions.push {
name: 'split'
type: 'split'
icon: 'split'
href: "#ticket/create/#{article.ticket_id}/#{article.id}"
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'split'
ui.navigate "#ticket/create/#{article.ticket_id}/#{article.id}"
true
App.Config.set('700-Split', Split, 'TicketZoomArticleAction')

View file

@ -0,0 +1,45 @@
class TelegramReply
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message'
actions.push {
name: 'reply'
type: 'telegramPersonalMessageReply'
icon: 'reply'
href: '#'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'telegramPersonalMessageReply'
ui.scrollToCompose()
# get reference article
type = App.TicketArticleType.find(article.type_id)
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
# get current body
articleNew.body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
position: 'end'
})
true
App.Config.set('300-TelegramReply', TelegramReply, 'TicketZoomArticleAction')

View file

@ -0,0 +1,128 @@
class TwitterReply
@action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer')
if article.type.name is 'twitter status'
actions.push {
name: 'reply'
type: 'twitterStatusReply'
icon: 'reply'
href: '#'
}
if article.type.name is 'twitter direct-message'
actions.push {
name: 'reply'
type: 'twitterDirectMessageReply'
icon: 'reply'
href: '#'
}
actions
@perform: (articleContainer, type, ticket, article, ui) ->
return true if type isnt 'twitterStatusReply' && type isnt 'twitterDirectMessageReply'
if type is 'twitterStatusReply'
@twitterStatusReply(ticket, article, ui)
else if type is 'twitterDirectMessageReply'
@twitterDirectMessageReply(ticket, article, ui)
true
@twitterStatusReply: (ticket, article, ui) ->
ui.scrollToCompose()
# get reference article
type = App.TicketArticleType.find(article.type_id)
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
# get current body
body = ui.el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
articleNew.body = body
recipients = article.from
if article.to
if recipients
recipients += ', '
recipients += article.to
if recipients
recipientString = ''
recipientScreenNames = recipients.split(',')
for recipientScreenName in recipientScreenNames
if recipientScreenName
recipientScreenName = recipientScreenName.trim().toLowerCase()
# exclude already listed screen name
exclude = false
if body && body.toLowerCase().match(recipientScreenName)
exclude = true
# exclude own screen_name
if recipientScreenName is "@#{ticket.preferences.channel_screen_name}".toLowerCase()
exclude = true
if exclude is false
if recipientString isnt ''
recipientString += ' '
recipientString += recipientScreenName
if body
articleNew.body = "#{recipientString} #{body}&nbsp;"
else
articleNew.body = "#{recipientString}&nbsp;"
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
position: 'end'
})
@twitterDirectMessageReply: (ticket, article, ui) ->
# get reference article
type = App.TicketArticleType.find(article.type_id)
sender = App.TicketArticleSender.find(article.sender_id)
customer = App.User.find(article.created_by_id)
ui.scrollToCompose()
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
if sender.name is 'Agent'
articleNew.to = article.to
else
articleNew.to = article.from
if !articleNew.to
articleNew.to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
App.Event.trigger('ui::ticket::setArticleType', {
ticket: ticket
type: type
article: articleNew
})
App.Config.set('300-TwitterReply', TwitterReply, 'TicketZoomArticleAction')

View file

@ -1,21 +1,13 @@
class App.TicketZoomArticleActions extends App.Controller
events:
'click [data-type=public]': 'publicInternal'
'click [data-type=internal]': 'publicInternal'
'click [data-type=emailReply]': 'emailReply'
'click [data-type=emailReplyAll]': 'emailReplyAll'
'click [data-type=twitterStatusReply]': 'twitterStatusReply'
'click [data-type=twitterDirectMessageReply]': 'twitterDirectMessageReply'
'click [data-type=facebookFeedReply]': 'facebookFeedReply'
'click [data-type=telegramPersonalMessageReply]': 'telegramPersonalMessageReply'
'click [data-type=delete]': 'delete'
'click .js-ArticleAction': 'actionPerform'
constructor: ->
super
@render()
render: ->
actions = @actionRow(@article)
actions = @actionRow(@ticket, @article)
if actions
@html App.view('ticket_zoom/article_view_actions')(
@ -25,371 +17,31 @@ class App.TicketZoomArticleActions extends App.Controller
else
@html ''
publicInternal: (e) =>
e.preventDefault()
articleContainer = $(e.target).closest('.ticket-article-item')
article_id = $(e.target).parents('[data-id]').data('id')
# storage update
article = App.TicketArticle.find(article_id)
internal = true
if article.internal == true
internal = false
@lastAttributres.internal = internal
article.updateAttributes(internal: internal)
# runntime update
if internal
articleContainer.addClass('is-internal')
else
articleContainer.removeClass('is-internal')
@render()
actionRow: (article) ->
if @permissionCheck('ticket.customer')
return []
actionRow: (ticket, article) ->
actionConfig = App.Config.get('TicketZoomArticleAction')
keys = _.keys(actionConfig).sort()
actions = []
if article.internal is true
actions = [
{
name: 'set to public'
type: 'public'
icon: 'lock-open'
}
]
else
actions = [
{
name: 'set to internal'
type: 'internal'
icon: 'lock'
}
]
#if @article.type.name is 'note'
# actions.push []
group = @ticket.group
if group.email_address_id && (article.type.name is 'email' || article.type.name is 'web')
actions.push {
name: 'reply'
type: 'emailReply'
icon: 'reply'
href: '#'
}
recipients = []
if article.sender.name is 'Customer'
if article.from
localRecipients = emailAddresses.parseAddressList(article.from)
if localRecipients
recipients = recipients.concat localRecipients
if article.to
localRecipients = emailAddresses.parseAddressList(article.to)
if localRecipients
recipients = recipients.concat localRecipients
if article.cc
localRecipients = emailAddresses.parseAddressList(article.cc)
if localRecipients
recipients = recipients.concat localRecipients
# remove system addresses
localAddresses = App.EmailAddress.all()
forgeinRecipients = []
recipientUsed = {}
for recipient in recipients
if !_.isEmpty(recipient.address)
localRecipientAddress = recipient.address.toString().toLowerCase()
if !recipientUsed[localRecipientAddress]
recipientUsed[localRecipientAddress] = true
localAddress = false
for address in localAddresses
if localRecipientAddress is address.email.toString().toLowerCase()
recipientUsed[localRecipientAddress] = true
localAddress = true
if !localAddress
forgeinRecipients.push recipient
# check if reply all is neede
if forgeinRecipients.length > 1
actions.push {
name: 'reply all'
type: 'emailReplyAll'
icon: 'reply-all'
href: '#'
}
if article.sender.name is 'Customer' && article.type.name is 'phone'
actions.push {
name: 'reply'
type: 'emailReply'
icon: 'reply'
href: '#'
}
if article.sender.name is 'Agent' && article.type.name is 'phone'
actions.push {
name: 'reply'
type: 'emailReply'
icon: 'reply'
href: '#'
}
if article.type.name is 'twitter status'
actions.push {
name: 'reply'
type: 'twitterStatusReply'
icon: 'reply'
href: '#'
}
if article.type.name is 'twitter direct-message'
actions.push {
name: 'reply'
type: 'twitterDirectMessageReply'
icon: 'reply'
href: '#'
}
if article.type.name is 'facebook feed post' || article.type.name is 'facebook feed comment'
actions.push {
name: 'reply'
type: 'facebookFeedReply'
icon: 'reply'
href: '#'
}
if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message'
actions.push {
name: 'reply'
type: 'telegramPersonalMessageReply'
icon: 'reply'
href: '#'
}
actions.push {
name: 'split'
type: 'split'
icon: 'split'
href: '#ticket/create/' + article.ticket_id + '/' + article.id
}
if article.type.name is 'note'
user = undefined
if App.Session.get('id') == article.created_by_id
user = App.User.find(App.Session.get('id'))
if user.permission('ticket.agent')
actions.push {
name: 'delete'
type: 'delete'
icon: 'trash'
href: '#'
}
for key in keys
config = actionConfig[key]
if config
actions = config.action(actions, ticket, article, @)
actions
facebookFeedReply: (e) =>
actionPerform: (e) =>
e.preventDefault()
type = App.TicketArticleType.findByAttribute('name', 'facebook feed comment')
@scrollToCompose()
articleContainer = $(e.target).closest('.ticket-article-item')
type = $(e.currentTarget).attr('data-type')
ticket = App.Ticket.fullLocal(@ticket.id)
article = App.TicketArticle.fullLocal(@article.id)
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
twitterStatusReply: (e) =>
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal(article_id)
sender = App.TicketArticleSender.find(article.sender_id)
type = App.TicketArticleType.find(article.type_id)
customer = App.User.find(article.created_by_id)
@scrollToCompose()
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
# get current body
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
articleNew.body = body
recipients = article.from
if article.to
if recipients
recipients += ', '
recipients += article.to
if recipients
recipientString = ''
recipientScreenNames = recipients.split(',')
for recipientScreenName in recipientScreenNames
if recipientScreenName
recipientScreenName = recipientScreenName.trim().toLowerCase()
# exclude already listed screen name
exclude = false
if body && body.toLowerCase().match(recipientScreenName)
exclude = true
# exclude own screen_name
if recipientScreenName is "@#{@ticket.preferences.channel_screen_name}".toLowerCase()
exclude = true
if exclude is false
if recipientString isnt ''
recipientString += ' '
recipientString += recipientScreenName
if body
articleNew.body = "#{recipientString} #{body}&nbsp;"
else
articleNew.body = "#{recipientString}&nbsp;"
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew, position: 'end' } )
twitterDirectMessageReply: (e) =>
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal(article_id)
type = App.TicketArticleType.find(article.type_id)
sender = App.TicketArticleSender.find(article.sender_id)
customer = App.User.find(article.created_by_id)
@scrollToCompose()
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
if sender.name is 'Agent'
articleNew.to = article.to
else
articleNew.to = article.from
if !articleNew.to
articleNew.to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
emailReplyAll: (e) =>
@emailReply(e, true)
emailReply: (e, all = false) =>
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal(article_id)
ticket = App.Ticket.fullLocal(article.ticket_id)
type = App.TicketArticleType.find(article.type_id)
article_created_by = App.User.find(article.created_by_id)
email_addresses = App.EmailAddress.all()
@scrollToCompose()
# empty form
articleNew = App.Utils.getRecipientArticle(ticket, article, article_created_by, type, email_addresses, all)
# get current body
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()
if !selected
selected = App.ClipBoard.getSelected('text')
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>"
# add selected text to body
body = selected + body
articleNew.body = body
type = App.TicketArticleType.findByAttribute(name:'email')
App.Event.trigger('ui::ticket::setArticleType', {
ticket: @ticket
type: type
article: articleNew
signaturePosition: signaturePosition
})
telegramPersonalMessageReply: (e) =>
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal(article_id)
sender = App.TicketArticleSender.find(article.sender_id)
type = App.TicketArticleType.find(article.type_id)
customer = App.User.find(article.created_by_id)
@scrollToCompose()
# empty form
articleNew = {
to: ''
cc: ''
body: ''
in_reply_to: ''
}
if article.message_id
articleNew.in_reply_to = article.message_id
# get current body
articleNew.body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew, position: 'end' } )
delete: (e) =>
e.preventDefault()
callback = ->
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.find(article_id)
article.destroy()
new App.ControllerConfirm(
message: 'Sure?'
callback: callback
container: @el.closest('.content')
)
actionConfig = App.Config.get('TicketZoomArticleAction')
keys = _.keys(actionConfig).sort()
actions = []
for key in keys
config = actionConfig[key]
if config
return if !config.perform(articleContainer, type, ticket, article, @)
scrollToCompose: =>
@el.closest('.content').find('.article-add').ScrollTo()

View file

@ -70,6 +70,14 @@ class App.TicketZoomArticleNew extends App.Controller
@textarea.focus()
)
# add article attachment
@bind('ui::ticket::addArticleAttachent', (data) =>
return if data.ticket.id.toString() isnt @ticket_id.toString()
return if _.isEmpty(data.attachments)
for file in data.attachments
@renderAttachment(file)
)
# reset new article screen
@bind('ui::ticket::taskReset', (data) =>
return if data.ticket_id.toString() isnt @ticket_id.toString()
@ -143,8 +151,8 @@ class App.TicketZoomArticleNew extends App.Controller
icon: 'twitter'
attributes: []
internal: false,
features: ['body:limit', 'body:initials']
maxTextLength: 140
features: attributes
maxTextLength: 280
warningTextLength: 30
}
if possibleArticleType['twitter direct-message']
@ -156,7 +164,7 @@ class App.TicketZoomArticleNew extends App.Controller
icon: 'twitter'
attributes: ['to']
internal: false,
features: ['body:limit', 'body:initials']
features: attributes
maxTextLength: 10000
warningTextLength: 500
}
@ -245,7 +253,7 @@ class App.TicketZoomArticleNew extends App.Controller
controller = new App.ControllerForm(
el: @$('.recipients')
model:
configure_attributes: configure_attributes,
configure_attributes: configure_attributes
)
@$('[data-name="body"]').ce({
@ -255,12 +263,13 @@ class App.TicketZoomArticleNew extends App.Controller
})
html5Upload.initialize(
uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload',
dropContainer: @$('.article-add').get(0),
cancelContainer: @cancelContainer,
inputField: @$('.article-attachment input').get(0),
key: 'File',
data: { form_id: @form_id },
uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload'
dropContainer: @$('.article-add').get(0)
cancelContainer: @cancelContainer
inputField: @$('.article-attachment input').get(0)
key: 'File'
data:
form_id: @form_id
maxSimultaneousUploads: 1,
onFileAdded: (file) =>
@ -303,6 +312,8 @@ class App.TicketZoomArticleNew extends App.Controller
)
)
@bindAttachmentDelete()
# show text module UI
if !@permissionCheck('ticket.customer')
textModule = new App.WidgetTextModule(
@ -737,33 +748,29 @@ class App.TicketZoomArticleNew extends App.Controller
@articleNewEdit.parent().removeClass('is-dropTarget') if @dragEventCounter is 0
renderAttachment: (file) =>
@attachmentsHolder.append App.view('generic/attachment_item')
fileName: file.filename
fileSize: @humanFileSize( file.size )
store_id: file.store_id
@attachmentsHolder.on(
'click'
"[data-id=#{file.store_id}]", (e) =>
@attachments = _.filter(
@attachments,
(item) ->
return if item.id isnt file.store_id
item
)
store_id = $(e.currentTarget).data('id')
@attachmentsHolder.append(App.view('generic/attachment_item')(file))
# delete attachment from storage
App.Ajax.request(
type: 'DELETE'
url: App.Config.get('api_path') + '/ticket_attachment_upload'
data: JSON.stringify(store_id: store_id)
processData: false
)
bindAttachmentDelete: =>
@attachmentsHolder.on('click', '.js-delete', (e) =>
id = $(e.currentTarget).data('id')
@attachments = _.filter(
@attachments,
(item) ->
return if item.id.toString() is id.toString()
item
)
# remove attachment from dom
element = $(e.currentTarget).closest('.attachments')
$(e.currentTarget).closest('.attachment').remove()
# empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o
if element.find('.attachment').length == 0
element.empty()
# delete attachment from storage
App.Ajax.request(
type: 'DELETE'
url: App.Config.get('api_path') + '/ticket_attachment_upload'
data: JSON.stringify(id: id)
processData: false
)
# remove attachment from dom
element = $(e.currentTarget).closest('.attachments')
$(e.currentTarget).closest('.attachment').remove()
if element.find('.attachment').length == 0
element.empty()
)

View file

@ -20,6 +20,7 @@ class App.TicketZoomArticleView extends App.Controller
el: el
ui: @ui
highligher: @highligher
form_id: @form_id
)
if !@ticketArticleInsertByIndex(index, el)
all.push el
@ -193,6 +194,7 @@ class ArticleViewItem extends App.ObserverController
ticket: @ticket
article: article
lastAttributres: @lastAttributres
form_id: @form_id
)
# set see more

View file

@ -32,16 +32,14 @@ class App.TicketZoomSidebar extends App.ObserverController
)
else
@sidebarBackends[key].reload(
params: @params
query: @query
params: @params
query: @query
formMeta: @formMeta
markForm: @markForm
tags: @tags
links: @links
)
item = @sidebarBackends[key].sidebarItem()
if item
@sidebarItems.push item
@sidebarItems.push @sidebarBackends[key]
new App.Sidebar(
el: @el.find('.tabsSidebar')

View file

@ -1,32 +1,67 @@
class SidebarCustomer extends App.Controller
sidebarItem: =>
return if !@permissionCheck('ticket.agent')
items = {
head: 'Customer'
name: 'customer'
icon: 'person'
actions: [
@item = {
name: 'customer'
badgeCallback: @badgeRender
sidebarHead: 'Customer'
sidebarCallback: @showCustomer
sidebarActions: [
{
title: 'Change Customer'
name: 'customer-change'
callback: @changeCustomer
},
]
callback: @showCustomer
}
return items if @ticket && @ticket.customer_id == 1
items.actions.push {
return @item if @ticket && @ticket.customer_id == 1
@item.sidebarActions.push {
title: 'Edit Customer'
name: 'customer-edit'
callback: @editCustomer
}
items
@item
metaBadge: (user) =>
counter = ''
cssClass = ''
counter = @sidebarItemCounter(user)
if @Config.get('ui_sidebar_open_ticket_indicator_colored') is true
if counter == 1
cssClass = 'tabsSidebar-tab-count--warning'
if counter > 1
cssClass = 'tabsSidebar-tab-count--danger'
{
name: 'customer'
icon: 'person'
counterPossible: true
counter: counter
cssClass: cssClass
}
badgeRender: (el) =>
@badgeEl = el
if App.User.exists(@ticket.customer_id)
user = App.User.find(@ticket.customer_id)
@badgeRenderLocal(user)
badgeRenderLocal: (user) =>
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge(user)))
sidebarItemCounter: (user) ->
counter = ''
if user && user.preferences && user.preferences.tickets_open
counter = user.preferences.tickets_open
counter
showCustomer: (el) =>
@el = el
@elSidebar = el
new App.WidgetUser(
el: @el
el: @elSidebar
user_id: @ticket.customer_id
callback: @badgeRenderLocal
)
editCustomer: =>
@ -38,13 +73,13 @@ class SidebarCustomer extends App.Controller
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
container: @elSidebar.closest('.content')
)
changeCustomer: =>
new App.TicketCustomer(
ticket_id: @ticket.id
container: @el.closest('.content')
container: @elSidebar.closest('.content')
)
App.Config.set('200-Customer', SidebarCustomer, 'TicketZoomSidebar')

View file

@ -1,19 +1,20 @@
class SidebarIdoit extends App.Controller
sidebarItem: =>
return if !@Config.get('idoit_integration')
{
head: 'i-doit'
name: 'idoit'
icon: 'printer'
actions: [
@item = {
name: 'idoit'
badgeIcon: 'printer'
sidebarHead: 'i-doit'
sidebarCallback: @showObjects
sidebarActions: [
{
title: 'Change Objects'
name: 'objects-change'
callback: @changeObjects
},
]
callback: @showObjects
}
@item
changeObjects: =>
new App.IdoitObjectSelector(

View file

@ -2,24 +2,25 @@ class SidebarOrganization extends App.Controller
sidebarItem: =>
return if !@permissionCheck('ticket.agent')
return if !@ticket.organization_id
{
head: 'Organization'
@item = {
name: 'organization'
icon: 'group'
actions: [
badgeIcon: 'group'
sidebarHead: 'Organization'
sidebarCallback: @showOrganization
sidebarActions: [
{
title: 'Edit Organization'
name: 'organization-edit'
callback: @editOrganization
},
]
callback: @showOrganization
}
@item
showOrganization: (el) =>
@el = el
@elSidebar = el
new App.WidgetOrganization(
el: @el
el: @elSidebar
organization_id: @ticket.organization_id
)
@ -31,7 +32,7 @@ class SidebarOrganization extends App.Controller
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
container: @elSidebar.closest('.content')
)
App.Config.set('300-Organization', SidebarOrganization, 'TicketZoomSidebar')

View file

@ -20,8 +20,9 @@ class Edit extends App.ObserverController
handlers: [
@ticketFormChanges
]
filter: @formMeta.filter
params: defaults
filter: @formMeta.filter
params: defaults
isDisabled: !ticket.editable()
#bookmarkable: true
)
@ -36,14 +37,14 @@ class Edit extends App.ObserverController
class SidebarTicket extends App.Controller
sidebarItem: =>
sidebarItem = {
head: 'Ticket'
name: 'ticket'
icon: 'message'
callback: @editTicket
@item = {
name: 'ticket'
badgeIcon: 'message'
sidebarHead: 'Ticket'
sidebarCallback: @editTicket
}
if @permissionCheck('ticket.agent')
sidebarItem['actions'] = [
@item.sidebarActions = [
{
title: 'History'
name: 'ticket-history'
@ -60,7 +61,7 @@ class SidebarTicket extends App.Controller
callback: @changeCustomer
},
]
sidebarItem
@item
reload: (args) =>
@ -79,7 +80,7 @@ class SidebarTicket extends App.Controller
editTicket: (el) =>
@el = el
localEl = $( App.view('ticket_zoom/sidebar_ticket')() )
localEl = $(App.view('ticket_zoom/sidebar_ticket')())
@edit = new Edit(
object_id: @ticket.id

View file

@ -145,7 +145,7 @@ class Index extends App.ControllerSubContent
query: @query
limit: 140
role_ids: role_ids
full: 1
full: true
processData: true,
success: (data, status, xhr) =>
App.Collection.loadAssets(data.assets)
@ -167,7 +167,7 @@ class Index extends App.ControllerSubContent
data:
limit: 50
role_ids: role_ids
full: 1
full: true
processData: true
success: (data, status, xhr) =>
App.Collection.loadAssets(data.assets)

View file

@ -0,0 +1,23 @@
class DefaultLocale extends App.Controller
constructor: ->
super
check = =>
preferences = App.Session.get('preferences')
return if !preferences
return if !_.isEmpty(preferences.locale)
locale = App.i18n.get()
@ajax(
id: "i18n-set-user-#{locale}"
type: 'PUT'
url: "#{App.Config.get('api_path')}/users/preferences"
data: JSON.stringify(locale: locale)
processData: true
)
App.Event.bind('auth:login', (session) =>
@delay(check, 3500, 'default_locale')
)
App.Config.set('default_locale', DefaultLocale, 'Widgets')

View file

@ -47,4 +47,4 @@ class Widget extends App.ControllerWidgetOnDemand
800
)
App.Config.set( 'switch_back_to_user', Widget, 'Widgets' )
App.Config.set('switch_back_to_user', Widget, 'Widgets')

View file

@ -70,6 +70,7 @@ class App.TicketStats extends App.Controller
render: (data) =>
if !data
data = @data
return if !data
user_total = 0
if data.user.open_ids && data.user.closed_ids

View file

@ -12,8 +12,8 @@ class App extends Spine.Controller
helper =
# define print name helper
P: (object, attributeName, attributes) ->
App.viewPrint(object, attributeName, attributes)
P: (object, attributeName, attributes, table = false) ->
App.viewPrint(object, attributeName, attributes, table)
# define date format helper
date: (time) ->
@ -136,7 +136,7 @@ class App extends Spine.Controller
return marked(string)
App.i18n.translateContent(string)
@viewPrint: (object, attributeName, attributes) ->
@viewPrint: (object, attributeName, attributes, table) ->
if !attributes
attributes = {}
if object.constructor.attributesGet
@ -172,10 +172,10 @@ class App extends Spine.Controller
if object[attributeNameWithoutRef]
valueRef = object[attributeNameWithoutRef]
@viewPrintItem(value, attributeConfig, valueRef)
@viewPrintItem(value, attributeConfig, valueRef, table)
# define print name helper
@viewPrintItem: (item, attributeConfig = {}, valueRef) ->
@viewPrintItem: (item, attributeConfig = {}, valueRef, table) ->
return '-' if item is undefined
return '-' if item is ''
return item if item is null
@ -238,7 +238,7 @@ class App extends Spine.Controller
# transform date
if attributeConfig.tag is 'date'
isHtmlEscape = true
resultLocal = App.i18n.translateDate(resultLocal)
resultLocal = App.i18n.translateDate(resultLocal)
# transform input tel|url to make it clickable
if attributeConfig.tag is 'input'
@ -258,8 +258,10 @@ class App extends Spine.Controller
cssClass = attributeConfig.class || ''
if cssClass.match 'escalation'
escalation = true
humanTime = App.PrettyDate.humanTime(resultLocal, escalation)
resultLocal = "<time class=\"humanTimeFromNow #{cssClass}\" data-time=\"#{resultLocal}\" title=\"#{timestamp}\">#{humanTime}</time>"
humanTime = ''
if !table
humanTime = App.PrettyDate.humanTime(resultLocal, escalation)
resultLocal = "<time class=\"humanTimeFromNow #{cssClass}\" data-time=\"#{resultLocal}\" title=\"#{timestamp}\">#{humanTime}</time>"
if !isHtmlEscape && typeof resultLocal is 'string'
resultLocal = App.Utils.htmlEscape(resultLocal)

View file

@ -77,7 +77,7 @@ class App._CollectionSingletonBase
callback: (data) =>
for counter, attr of @callbacks
callback = ->
callback = =>
attr.callback(data)
if attr.one
delete @callbacks[counter]

View file

@ -71,7 +71,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
@open()
focusInput: =>
@objectSelect.focus() if not @formControl.hasClass 'focus'
@objectSelect.focus() if not @formControl.hasClass('focus')
onBlur: =>
selectObject = @objectSelect.val()
@ -85,6 +85,9 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
@objectId.val("guess:#{selectObject}")
@formControl.removeClass 'focus'
resetObjectSelection: =>
@objectId.val('').trigger('change')
onObjectClick: (e) =>
objectId = $(e.currentTarget).data('object-id')
@selectObject(objectId)
@ -103,23 +106,23 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
# Only work with the last one since its the newest one
objectId = @objectId.val().split(',').pop()
return if !objectId
return if !App[@objectSingle].exists(objectId)
object = App[@objectSingle].find(objectId)
name = object.displayName()
if objectId && App[@objectSingle].exists(objectId)
object = App[@objectSingle].find(objectId)
name = object.displayName()
if @attribute.multiple
# create token
@createToken name, objectId
else
if object.email
if @attribute.multiple
# quote name for special character
if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/)
name = "\"#{name}\""
name += " <#{object.email}>"
# create token
@createToken(name, objectId)
else
if object.email
@objectSelect.val(name)
# quote name for special character
if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/)
name = "\"#{name}\""
name += " <#{object.email}>"
@objectSelect.val(name)
if @callback
@callback(objectId)
@ -321,12 +324,16 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
@hideOrganizationMembers()
# hide dropdown
if !query
if _.isEmpty(query)
@emptyResultList()
if !@attribute.disableCreateObject
@recipientList.append(@buildObjectNew())
# reset object selection
@resetObjectSelection()
return
# show dropdown
if query && ( !@attribute.minLengt || @attribute.minLengt <= query.length )
@lazySearch(query)

View file

@ -79,8 +79,7 @@ class App.Auth
@_updateModelAttributes(data.models)
# set locale
locale = window.navigator.userLanguage || window.navigator.language || 'en-us'
App.i18n.set(locale)
App.i18n.set(App.i18n.detectBrowserLocale())
# rebuild navbar with new navbar items
App.Event.trigger('auth')
@ -120,7 +119,7 @@ class App.Auth
if preferences && preferences.locale
locale = preferences.locale
if !locale
locale = window.navigator.userLanguage || window.navigator.language || 'en-us'
locale = App.i18n.detectBrowserLocale()
App.i18n.set(locale)
App.Event.trigger('auth:login', data.session)

View file

@ -39,14 +39,27 @@ class App.ColumnSelect extends Spine.Controller
)
render: ->
if !_.isEmpty(@attribute.seperator)
values = []
if @attribute.value
values = @attribute.value.split(';')
else if @attribute.default
values = @attribute.default.split(';')
for value in values
for option in @options.attribute.options
if option.value is value
option.selected = true
@values = []
_.each @options.attribute.options, (option) =>
if option.selected
@values.push option.value.toString()
@html App.view('generic/column_select')
@html App.view('generic/column_select')(
attribute: @options.attribute
values: @values
)
# keep inital height
# disabled for now since controls in modals get rendered hidden
@ -60,13 +73,17 @@ class App.ColumnSelect extends Spine.Controller
@throttledSelect()
select: (value) ->
@selected.find("[data-value='#{value}']").removeClass 'is-hidden'
@pool.find("[data-value='#{value}']").addClass 'is-hidden'
@selected.find("[data-value='#{value}']").removeClass('is-hidden')
@pool.find("[data-value='#{value}']").addClass('is-hidden')
@values.push(value)
@shadow.val(@values)
@shadow.trigger('change')
@placeholder.addClass 'is-hidden'
if !_.isEmpty(@attribute.seperator)
@shadow.val(@values.join(';'))
else
@shadow.val(@values)
@shadow.trigger('change')
@placeholder.addClass('is-hidden')
if @search.val() and @poolOptions.not('.is-filtered').not('.is-hidden').size() is 0
@clear()
@ -76,14 +93,17 @@ class App.ColumnSelect extends Spine.Controller
@throttledRemove()
remove: (value) ->
@pool.find("[data-value='#{value}']").removeClass 'is-hidden'
@selected.find("[data-value='#{value}']").addClass 'is-hidden'
@pool.find("[data-value='#{value}']").removeClass('is-hidden')
@selected.find("[data-value='#{value}']").addClass('is-hidden')
@values.splice(@values.indexOf(value), 1)
@shadow.val(@values)
@shadow.trigger('change')
if !_.isEmpty(@attribute.seperator)
@shadow.val(@values.join(';'))
else
@shadow.val(@values)
@shadow.trigger('change')
if !@values.length
@placeholder.removeClass 'is-hidden'
@placeholder.removeClass('is-hidden')
filter: (event) ->
filter = $(event.currentTarget).val()
@ -92,16 +112,16 @@ class App.ColumnSelect extends Spine.Controller
return if $(el).hasClass('is-hidden')
if $(el).text().toLowerCase().indexOf(filter.toLowerCase()) > -1
$(el).removeClass 'is-filtered'
$(el).removeClass('is-filtered')
else
$(el).addClass 'is-filtered'
$(el).addClass('is-filtered')
@clearButton.toggleClass 'is-hidden', filter.length is 0
clear: ->
@search.val('')
@poolOptions.removeClass 'is-filtered'
@clearButton.addClass 'is-hidden'
@poolOptions.removeClass('is-filtered')
@clearButton.addClass('is-hidden')
onFilterKeydown: (event) ->
return if event.keyCode != 13
@ -111,4 +131,4 @@ class App.ColumnSelect extends Spine.Controller
firstVisibleOption = @poolOptions.not('.is-filtered').not('.is-hidden').first()
if firstVisibleOption
@select firstVisibleOption.attr('data-value')
@select firstVisibleOption.attr('data-value')

View file

@ -80,6 +80,24 @@ class App.i18n
_instance ?= new _i18nSingleton()
_instance.mapTime
@detectBrowserLocale: ->
return 'en-us' if !window.navigator.userLanguage && !window.navigator.language
if window.navigator.languages
allLocales = App.Locale.all()
for browserLocale in window.navigator.languages
for localAllLocale in allLocales
if browserLocale is localAllLocale.locale
return localAllLocale.locale
for browserLocale in window.navigator.languages
browserLocale = browserLocale.substr(0, 2)
for localAllLocale in allLocales
if browserLocale is localAllLocale.alias
return localAllLocale.locale
window.navigator.userLanguage || window.navigator.language || 'en-us'
class _i18nSingleton extends Spine.Module
@include App.LogInclude
@ -319,18 +337,20 @@ class _i18nSingleton extends Spine.Module
if offset
timeObject = new Date(timeObject.getTime() + (timeObject.getTimezoneOffset() * 60000))
d = timeObject.getDate()
m = timeObject.getMonth() + 1
y = timeObject.getFullYear()
S = timeObject.getSeconds()
M = timeObject.getMinutes()
H = timeObject.getHours()
d = timeObject.getDate()
m = timeObject.getMonth() + 1
yfull = timeObject.getFullYear()
yshort = timeObject.getYear()-100
S = timeObject.getSeconds()
M = timeObject.getMinutes()
H = timeObject.getHours()
format = format
.replace(/dd/, s(d, 2))
.replace(/d/, d)
.replace(/mm/, s(m, 2))
.replace(/m/, m)
.replace(/yyyy/, y)
.replace(/yyyy/, yfull)
.replace(/yy/, yshort)
.replace(/SS/, s(S, 2))
.replace(/MM/, s(M, 2))
.replace(/HH/, s(H, 2))

View file

@ -20,37 +20,53 @@ class App.ImageService
imageWidth = imageObject.width
imageHeight = imageObject.height
console.log('ImageService', 'current size', imageWidth, imageHeight)
console.log('ImageService', 'sizeFactor', sizeFactor)
if y is 'auto' && x is 'auto'
x = imageWidth
y = imageHeight
# set max x/y
if x isnt 'auto' && x > imageWidth
x = imageWidth
if y isnt 'auto' && y > imageHeight
y = imageHeight
# get auto dimensions
if y is 'auto'
if y is 'auto'# && (y * factor) >= imageHeight
factor = imageWidth / x
y = imageHeight / factor
if x is 'auto'
if x is 'auto'# && (y * factor) >= imageWidth
factor = imageWidth / y
x = imageHeight / factor
canvas = document.createElement('canvas')
# check if resize is needed
resize = false
if x < imageWidth || y < imageHeight
if (x < imageWidth && (x * sizeFactor < imageWidth)) || (y < imageHeight && (y * sizeFactor < imageHeight))
resize = true
x = x * sizeFactor
y = y * sizeFactor
# set dimensions
canvas.width = x
canvas.height = y
# draw image on canvas and set image dimensions
context = canvas.getContext('2d')
context.drawImage(imageObject, 0, 0, x, y)
else
x = imageWidth
y = imageHeight
# create canvas and set dimensions
canvas = document.createElement('canvas')
canvas.width = x
canvas.height = y
# set dimensions
canvas.width = imageWidth
canvas.height = imageHeight
# draw image on canvas and set image dimensions
context = canvas.getContext('2d')
context.drawImage(imageObject, 0, 0, x, y)
# draw image on canvas and set image dimensions
context = canvas.getContext('2d')
context.drawImage(imageObject, 0, 0, imageWidth, imageHeight)
# set quallity based on image size
if quallity == 'auto'

View file

@ -5,6 +5,8 @@ class App.Utils
'TD': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'valign', 'width', 'style']
'TH': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'scope', 'sorted', 'valign', 'width', 'style']
'TR': ['width', 'style']
'A': ['href', 'hreflang', 'name', 'rel']
'IMG': ['align', 'alt', 'border', 'height', 'src', 'srcset', 'width', 'style']
@mapCss:
'TABLE': [
@ -14,15 +16,9 @@ class App.Utils
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
]
'TH': [
'background', 'background-color', 'color', 'font-size', 'vertical-align',
@ -31,15 +27,10 @@ class App.Utils
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]
'TR': [
'background', 'background-color', 'color', 'font-size', 'vertical-align',
@ -48,15 +39,10 @@ class App.Utils
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]
'TD': [
'background', 'background-color', 'color', 'font-size', 'vertical-align',
@ -65,15 +51,13 @@ class App.Utils
'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
]
'IMG': [
'width', 'height',
]
# textCleand = App.Utils.textCleanup(rawText)
@ -230,7 +214,7 @@ class App.Utils
# remove comments
@_removeComments(html)
# remove work markup
# remove word markup
@_removeWordMarkup(html)
# remove tags, keep content
@ -251,7 +235,7 @@ class App.Utils
# remove comments
@_removeComments(html)
# remove work markup
# remove word markup
@_removeWordMarkup(html)
# remove tags, keep content
@ -275,11 +259,11 @@ class App.Utils
# remove comments
@_removeComments(html)
# remove work markup
# remove word markup
@_removeWordMarkup(html)
# remove tags, keep content
html.find('a, font, small, time, form, label').replaceWith( ->
html.find('font, small, time, form, label').replaceWith( ->
$(@).contents()
)
@ -303,7 +287,7 @@ class App.Utils
)
# remove tags & content
html.find('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove()
html.find('font, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove()
# remove style and class
@_cleanAttributes(html)
@ -906,6 +890,29 @@ class App.Utils
text = text.replace(/http(s|):\/\/[-A-Za-z0-9+&@#\/%?=~_\|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/img, placeholder)
text.length
@parseAddressListLocal: (line) ->
recipients = emailAddresses.parseAddressList(line)
result = []
if !_.isEmpty(recipients)
for recipient in recipients
if recipient && recipient.address
result.push recipient.address
return result
# workaround for email-addresses.js issue with this kind of
# mail headers "From: invalid sender, realname <sender@example.com>"
# email-addresses.js is returning null because it can't parse the
# whole header
if _.isEmpty(recipients) && line.match('@')
recipients = line.split(',')
re = /(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
for recipient in recipients
if recipient && recipient.match('@')
localResult = recipient.match(re)
if localResult && localResult[0]
result.push localResult[0]
result
@getRecipientArticle: (ticket, article, article_created_by, type, email_addresses = [], all) ->
# empty form
@ -954,16 +961,18 @@ class App.Utils
# check if article sender is local
senderIsLocal = false
if !_.isEmpty(article.from)
senders = emailAddresses.parseAddressList(article.from)
if senders && senders[0] && senders[0].address
senderIsLocal = isLocalAddress(senders[0].address)
senders = App.Utils.parseAddressListLocal(article.from)
if senders
for sender in senders
if sender && sender.match('@')
senderIsLocal = isLocalAddress(sender)
# check if article recipient is local
recipientIsLocal = false
if !_.isEmpty(article.to)
recipients = emailAddresses.parseAddressList(article.to)
if recipients && recipients[0] && recipients[0].address
recipientIsLocal = isLocalAddress(recipients[0].address)
recipients = App.Utils.parseAddressListLocal(article.to)
if recipients && recipients[0]
recipientIsLocal = isLocalAddress(recipients[0])
# sender is local
if senderIsLocal
@ -987,14 +996,14 @@ class App.Utils
# filter for uniq recipients
recipientAddresses = {}
addAddresses = (addressLine, line) ->
lineNew = ''
recipients = emailAddresses.parseAddressList(addressLine)
recipients = App.Utils.parseAddressListLocal(addressLine)
if !_.isEmpty(recipients)
for recipient in recipients
if !_.isEmpty(recipient.address)
localRecipientAddress = recipient.address.toString().toLowerCase()
if !_.isEmpty(recipient)
localRecipientAddress = recipient.toString().toLowerCase()
# check if address is not local
if !isLocalAddress(localRecipientAddress)

View file

@ -161,7 +161,14 @@ window.linkify = (function(){
}
// Push massaged link onto the array
parts.push([ link, href ]);
// 2018-10-30: me only link urls, not mailto link
//parts.push([ link, href ]);
if ( href && href.substr && href.substr(0,7) != 'mailto:') {
parts.push([ link, href ]);
}
else {
parts.push([ link, undefined ]);
}
};
// Push remaining non-link text onto the array.

View file

@ -1,6 +1,6 @@
// email-addresses.js - RFC 5322 email address parser
// v 2.0.1
// v 3.0.1
//
// http://tools.ietf.org/html/rfc5322
//
@ -186,27 +186,7 @@ function parse5322(opts) {
// "First Last" -> "First Last"
// "First Last" -> "First Last"
function collapseWhitespace(s) {
function isWhitespace(c) {
return c === ' ' ||
c === '\t' ||
c === '\r' ||
c === '\n';
}
var i, str;
str = "";
for (i = 0; i < s.length; i += 1) {
if (!isWhitespace(s[i]) || !isWhitespace(s[i + 1])) {
str += s[i];
}
}
if (isWhitespace(str[0])) {
str = str.substring(1);
}
if (isWhitespace(str[str.length - 1])) {
str = str.substring(0, str.length - 1);
}
return str;
return s.replace(/([ \t]|\r\n)+/g, ' ').replace(/^\s*/, '').replace(/\s*$/, '');
}
// UTF-8 pseudo-production (RFC 6532)
@ -597,10 +577,14 @@ function parse5322(opts) {
return wrap('domain', function domainCheckTLD() {
var result = or(obsDomain, dotAtom, domainLiteral)();
if (opts.rejectTLD) {
if (result.semantic.indexOf('.') < 0) {
if (result && result.semantic && result.semantic.indexOf('.') < 0) {
return null;
}
}
// strip all whitespace from domains
if (result) {
result.semantic = result.semantic.replace(/\s+/g, '');
}
return result;
}());
}
@ -612,6 +596,36 @@ function parse5322(opts) {
)());
}
// 3.6.2 Originator Fields
// Below we only parse the field body, not the name of the field
// like "From:", "Sender:", or "Reply-To:". Other libraries that
// parse email headers can parse those and defer to these productions
// for the "RFC 5322" part.
// RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields
// from = "From:" (mailbox-list / address-list) CRLF
function fromSpec() {
return wrap('from', or(
mailboxList,
addressList
)());
}
// RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields
// sender = "Sender:" (mailbox / address) CRLF
function senderSpec() {
return wrap('sender', or(
mailbox,
address
)());
}
// RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields
// reply-to = "Reply-To:" address-list CRLF
function replyToSpec() {
return wrap('reply-to', addressList());
}
// 4.1. Miscellaneous Obsolete Tokens
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
@ -766,92 +780,186 @@ function parse5322(opts) {
// ast analysis
function findNode(name, root) {
var i, queue, node;
var i, stack, node;
if (root === null || root === undefined) { return null; }
queue = [root];
while (queue.length > 0) {
node = queue.shift();
stack = [root];
while (stack.length > 0) {
node = stack.pop();
if (node.name === name) {
return node;
}
for (i = 0; i < node.children.length; i += 1) {
queue.push(node.children[i]);
for (i = node.children.length - 1; i >= 0; i -= 1) {
stack.push(node.children[i]);
}
}
return null;
}
function findAllNodes(name, root) {
var i, queue, node, result;
var i, stack, node, result;
if (root === null || root === undefined) { return null; }
queue = [root];
stack = [root];
result = [];
while (queue.length > 0) {
node = queue.shift();
while (stack.length > 0) {
node = stack.pop();
if (node.name === name) {
result.push(node);
}
for (i = 0; i < node.children.length; i += 1) {
queue.push(node.children[i]);
for (i = node.children.length - 1; i >= 0; i -= 1) {
stack.push(node.children[i]);
}
}
return result;
}
function findAllNodesNoChildren(names, root) {
var i, stack, node, result, namesLookup;
if (root === null || root === undefined) { return null; }
stack = [root];
result = [];
namesLookup = {};
for (i = 0; i < names.length; i += 1) {
namesLookup[names[i]] = true;
}
while (stack.length > 0) {
node = stack.pop();
if (node.name in namesLookup) {
result.push(node);
// don't look at children (hence findAllNodesNoChildren)
} else {
for (i = node.children.length - 1; i >= 0; i -= 1) {
stack.push(node.children[i]);
}
}
}
return result;
}
function giveResult(ast) {
function grabSemantic(n) {
return n !== null ? n.semantic : null;
}
var i, ret, addresses, addr, name, aspec, local, domain;
var addresses, groupsAndMailboxes, i, groupOrMailbox, result;
if (ast === null) {
return null;
}
ret = { ast: ast };
addresses = findAllNodes('address', ast);
ret.addresses = [];
for (i = 0; i < addresses.length; i += 1) {
addr = addresses[i];
name = findNode('display-name', addr);
aspec = findNode('addr-spec', addr);
local = findNode('local-part', aspec);
domain = findNode('domain', aspec);
ret.addresses.push({
node: addr,
parts: {
name: name,
address: aspec,
local: local,
domain: domain
},
name: grabSemantic(name),
address: grabSemantic(aspec),
local: grabSemantic(local),
domain: grabSemantic(domain)
});
}
addresses = [];
if (opts.simple) {
ret = ret.addresses;
for (i = 0; i < ret.length; i += 1) {
delete ret[i].node;
// An address is a 'group' (i.e. a list of mailboxes) or a 'mailbox'.
groupsAndMailboxes = findAllNodesNoChildren(['group', 'mailbox'], ast);
for (i = 0; i < groupsAndMailboxes.length; i += 1) {
groupOrMailbox = groupsAndMailboxes[i];
if (groupOrMailbox.name === 'group') {
addresses.push(giveResultGroup(groupOrMailbox));
} else if (groupOrMailbox.name === 'mailbox') {
addresses.push(giveResultMailbox(groupOrMailbox));
}
}
return ret;
result = {
ast: ast,
addresses: addresses,
};
if (opts.simple) {
result = simplifyResult(result);
}
if (opts.oneResult) {
return oneResult(result);
}
if (opts.simple) {
return result && result.addresses;
} else {
return result;
}
}
function giveResultGroup(group) {
var i;
var groupName = findNode('display-name', group);
var groupResultMailboxes = [];
var mailboxes = findAllNodesNoChildren(['mailbox'], group);
for (i = 0; i < mailboxes.length; i += 1) {
groupResultMailboxes.push(giveResultMailbox(mailboxes[i]));
}
return {
node: group,
parts: {
name: groupName,
},
type: group.name, // 'group'
name: grabSemantic(groupName),
addresses: groupResultMailboxes,
};
}
function giveResultMailbox(mailbox) {
var name = findNode('display-name', mailbox);
var aspec = findNode('addr-spec', mailbox);
var comments = findAllNodes('cfws', mailbox);
var local = findNode('local-part', aspec);
var domain = findNode('domain', aspec);
return {
node: mailbox,
parts: {
name: name,
address: aspec,
local: local,
domain: domain,
comments: comments
},
type: mailbox.name, // 'mailbox'
name: grabSemantic(name),
address: grabSemantic(aspec),
local: grabSemantic(local),
domain: grabSemantic(domain),
groupName: grabSemantic(mailbox.groupName),
};
}
function grabSemantic(n) {
return n !== null && n !== undefined ? n.semantic : null;
}
function simplifyResult(result) {
var i;
if (result && result.addresses) {
for (i = 0; i < result.addresses.length; i += 1) {
delete result.addresses[i].node;
}
}
return result;
}
function oneResult(result) {
if (!result) { return null; }
if (!opts.partial && result.addresses.length > 1) { return null; }
return result.addresses && result.addresses[0];
}
/////////////////////////////////////////////////////
var parseString, pos, len, parsed;
var parseString, pos, len, parsed, startProduction;
opts = handleOpts(opts, {});
if (opts === null) { return null; }
parseString = opts.input;
startProduction = {
'address': address,
'address-list': addressList,
'angle-addr': angleAddr,
'from': fromSpec,
'group': group,
'mailbox': mailbox,
'mailbox-list': mailboxList,
'reply-to': replyToSpec,
'sender': senderSpec,
}[opts.startAt] || addressList;
if (!opts.strict) {
initialize();
opts.strict = true;
parsed = addressList(parseString);
parsed = startProduction(parseString);
if (opts.partial || !inStr()) {
return giveResult(parsed);
}
@ -859,46 +967,51 @@ function parse5322(opts) {
}
initialize();
parsed = addressList(parseString);
parsed = startProduction(parseString);
if (!opts.partial && inStr()) { return null; }
return giveResult(parsed);
}
function parseOneAddressSimple(opts) {
var result;
opts = handleOpts(opts, {
return parse5322(handleOpts(opts, {
oneResult: true,
rfc6532: true,
simple: true
});
if (opts === null) { return null; }
result = parse5322(opts);
if ((!result) ||
(!opts.partial &&
(opts.simple && result.length > 1) ||
(!opts.simple && result.addresses.length > 1))) {
return null;
}
return opts.simple ?
result && result[0] :
result && result.addresses && result.addresses[0];
simple: true,
startAt: 'address-list',
}));
}
function parseAddressListSimple(opts) {
var result;
opts = handleOpts(opts, {
return parse5322(handleOpts(opts, {
rfc6532: true,
simple: true
});
if (opts === null) { return null; }
simple: true,
startAt: 'address-list',
}));
}
result = parse5322(opts);
function parseFromSimple(opts) {
return parse5322(handleOpts(opts, {
rfc6532: true,
simple: true,
startAt: 'from',
}));
}
return opts.simple ? result : result.addresses;
function parseSenderSimple(opts) {
return parse5322(handleOpts(opts, {
oneResult: true,
rfc6532: true,
simple: true,
startAt: 'sender',
}));
}
function parseReplyToSimple(opts) {
return parse5322(handleOpts(opts, {
rfc6532: true,
simple: true,
startAt: 'reply-to',
}));
}
function handleOpts(opts, defs) {
@ -926,24 +1039,28 @@ function handleOpts(opts, defs) {
if (!defs) { return null; }
defaults = {
rfc6532: false,
oneResult: false,
partial: false,
rejectTLD: false,
rfc6532: false,
simple: false,
startAt: 'address-list',
strict: false,
rejectTLD: false
};
for (o in defaults) {
if (isNullUndef(opts[o])) {
opts[o] = !isNullUndef(defs[o]) ? defs[o] : defaults[o];
}
opts[o] = !!opts[o];
}
return opts;
}
parse5322.parseOneAddress = parseOneAddressSimple;
parse5322.parseAddressList = parseAddressListSimple;
parse5322.parseFrom = parseFromSimple;
parse5322.parseSender = parseSenderSimple;
parse5322.parseReplyTo = parseReplyToSimple;
// in Zammad context, go back to non CommonJS
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {

View file

@ -289,15 +289,16 @@
var result = e.target.result
var img = document.createElement('img')
img.src = result
maxWidth = _this.$element.width() || 500
scaleFactor = 2
//scaleFactor = 1
//if (window.isRetina && window.isRetina()) {
// scaleFactor = 2
//}
insert = function(dataUrl, width, height, isRetina) {
insert = function(dataUrl, width, height, isResized) {
//console.log('dataUrl', dataUrl)
// adapt image if we are on retina devices
if (!isRetina && window.isRetina && window.isRetina()) {
width = width / 2
height = height / 2
}
//console.log('scaleFactor', scaleFactor, isResized, maxWidth, width, height)
_this.log('image inserted')
result = dataUrl
if (_this.options.imageWidth == 'absolute') {
@ -310,7 +311,7 @@
}
// resize if to big
App.ImageService.resize(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
}
reader.readAsDataURL(imageFile)
imageInserted = true
@ -416,17 +417,18 @@
var result = e.target.result
var img = document.createElement('img')
img.src = result
maxWidth = _this.$element.width() || 500
scaleFactor = 2
//scaleFactor = 1
//if (window.isRetina && window.isRetina()) {
// scaleFactor = 2
//}
//Insert the image at the carat
insert = function(dataUrl, width, height, isRetina) {
// adapt image if we are on retina devices
if (!isRetina && window.isRetina && window.isRetina()) {
width = width / 2
height = height / 2
}
insert = function(dataUrl, width, height, isResized) {
//console.log('dataUrl', dataUrl)
//console.log('scaleFactor', scaleFactor, isResized, maxWidth, width, height)
_this.log('image inserted')
result = dataUrl
if (_this.options.imageWidth == 'absolute') {
@ -454,7 +456,7 @@
}
// resize if to big
App.ImageService.resize(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
})
reader.readAsDataURL(file)
}

View file

@ -74,8 +74,9 @@ window.word_filter = function(editor){
}
})
$('[style]', editor).removeAttr('style');
$('[align]', editor).removeAttr('align');
// style and align is handled by utils.coffee it self, don't clean it here
//$('[style]', editor).removeAttr('style');
//$('[align]', editor).removeAttr('align');
$('span', editor).replaceWith(function() {return $(this).contents();});
$('span:empty', editor).remove();
$("[class^='Mso']", editor).removeAttr('class');

View file

@ -26,6 +26,7 @@
- allow custom template as options parameter
- fix that place method doesn't think that the container is the window, but rather the real window is the window
- added rerender method to show correct today if task is longer open the 24 hours
- scroll into view
*/
(function(factory){
@ -515,7 +516,9 @@
)
)
this.setValue();
this._trigger('hide');
// 2018-01-22 trigger locale hide event - conflicts with modal hide
//this._trigger('hide');
this._trigger('hide.bs.datepicker');
return this;
},
@ -757,6 +760,16 @@
zIndex: zIndex
});
}
// adjust scroll of scrollParent
var scrollParent = this.picker.scrollParent();
var bottomEdge = offset.top + height + this.picker.outerHeight();
var scrollBottomEdge = scrollParent.scrollTop() + scrollParent.height();
if(bottomEdge > scrollBottomEdge){
scrollParent.scrollTop(scrollParent.scrollTop() + (bottomEdge - scrollBottomEdge) + 10);
}
return this;
},

View file

@ -387,15 +387,17 @@ set new attributes of model (remove already available attributes)
=>
return if _.isEmpty(@SUBSCRIPTION_COLLECTION)
App.Log.debug('Model', "server notify collection change #{@className}")
@fetchFull(
->
clear: true
)
callback = =>
@fetchFull(
->
clear: true
)
App.Delay.set(callback, 200, "full-#{@className}")
"Collection::Subscribe::#{@className}"
)
key = @className + '-' + Math.floor( Math.random() * 99999 )
key = "#{@className}-#{Math.floor(Math.random() * 99999)}"
@SUBSCRIPTION_COLLECTION[key] = callback
# fetch init collection

View file

@ -1,16 +1,260 @@
class App.Chat extends App.Model
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'note'
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'block_country', 'note'
@extend Spine.Model.Ajax
@url: @apiPath + '/chats'
@countries:
AF: 'Afghanistan'
AL: 'Albania'
DZ: 'Algeria'
AS: 'American Samoa'
AD: 'Andorra'
AO: 'Angola'
AI: 'Anguilla'
AQ: 'Antarctica'
AG: 'Antigua And Barbuda'
AR: 'Argentina'
AM: 'Armenia'
AW: 'Aruba'
AU: 'Australia'
AT: 'Austria'
AZ: 'Azerbaijan'
BS: 'Bahamas'
BH: 'Bahrain'
BD: 'Bangladesh'
BB: 'Barbados'
BY: 'Belarus'
BE: 'Belgium'
BZ: 'Belize'
BJ: 'Benin'
BM: 'Bermuda'
BT: 'Bhutan'
BO: 'Bolivia'
BA: 'Bosnia And Herzegovina'
BW: 'Botswana'
BV: 'Bouvet Island'
BR: 'Brazil'
IO: 'British Indian Ocean Territory'
BN: 'Brunei Darussalam'
BG: 'Bulgaria'
BF: 'Burkina Faso'
BI: 'Burundi'
KH: 'Cambodia'
CM: 'Cameroon'
CA: 'Canada'
CV: 'Cape Verde'
KY: 'Cayman Islands'
CF: 'Central African Republic'
TD: 'Chad'
CL: 'Chile'
CN: 'China'
CX: 'Christmas Island'
CC: 'Cocos (keeling) Islands'
CO: 'Colombia'
KM: 'Comoros'
CG: 'Congo'
CD: 'Congo, The Democratic Republic Of The'
CK: 'Cook Islands'
CR: 'Costa Rica'
CI: 'Cote D\'ivoire'
HR: 'Croatia'
CU: 'Cuba'
CY: 'Cyprus'
CZ: 'Czech Republic'
DK: 'Denmark'
DJ: 'Djibouti'
DM: 'Dominica'
DO: 'Dominican Republic'
TP: 'East Timor'
EC: 'Ecuador'
EG: 'Egypt'
SV: 'El Salvador'
GQ: 'Equatorial Guinea'
ER: 'Eritrea'
EE: 'Estonia'
ET: 'Ethiopia'
FK: 'Falkland Islands (malvinas)'
FO: 'Faroe Islands'
FJ: 'Fiji'
FI: 'Finland'
FR: 'France'
GF: 'French Guiana'
PF: 'French Polynesia'
TF: 'French Southern Territories'
GA: 'Gabon'
GM: 'Gambia'
GE: 'Georgia'
DE: 'Germany'
GH: 'Ghana'
GI: 'Gibraltar'
GR: 'Greece'
GL: 'Greenland'
GD: 'Grenada'
GP: 'Guadeloupe'
GU: 'Guam'
GT: 'Guatemala'
GN: 'Guinea'
GW: 'Guinea-bissau'
GY: 'Guyana'
HT: 'Haiti'
HM: 'Heard Island And Mcdonald Islands'
VA: 'Holy See (vatican City State)'
HN: 'Honduras'
HK: 'Hong Kong'
HU: 'Hungary'
IS: 'Iceland'
IN: 'India'
ID: 'Indonesia'
IR: 'Iran, Islamic Republic Of'
IQ: 'Iraq'
IE: 'Ireland'
IL: 'Israel'
IT: 'Italy'
JM: 'Jamaica'
JP: 'Japan'
JO: 'Jordan'
KZ: 'Kazakstan'
KE: 'Kenya'
KI: 'Kiribati'
KP: 'Korea, Democratic People\'s Republic Of'
KR: 'Korea, Republic Of'
KV: 'Kosovo'
KW: 'Kuwait'
KG: 'Kyrgyzstan'
LA: 'Lao People\'s Democratic Republic'
LV: 'Latvia'
LB: 'Lebanon'
LS: 'Lesotho'
LR: 'Liberia'
LY: 'Libyan Arab Jamahiriya'
LI: 'Liechtenstein'
LT: 'Lithuania'
LU: 'Luxembourg'
MO: 'Macau'
MK: 'Macedonia, The Former Yugoslav Republic Of'
MG: 'Madagascar'
MW: 'Malawi'
MY: 'Malaysia'
MV: 'Maldives'
ML: 'Mali'
MT: 'Malta'
MH: 'Marshall Islands'
MQ: 'Martinique'
MR: 'Mauritania'
MU: 'Mauritius'
YT: 'Mayotte'
MX: 'Mexico'
FM: 'Micronesia, Federated States Of'
MD: 'Moldova, Republic Of'
MC: 'Monaco'
MN: 'Mongolia'
MS: 'Montserrat'
ME: 'Montenegro'
MA: 'Morocco'
MZ: 'Mozambique'
MM: 'Myanmar'
NA: 'Namibia'
NR: 'Nauru'
NP: 'Nepal'
NL: 'Netherlands'
AN: 'Netherlands Antilles'
NC: 'New Caledonia'
NZ: 'New Zealand'
NI: 'Nicaragua'
NE: 'Niger'
NG: 'Nigeria'
NU: 'Niue'
NF: 'Norfolk Island'
MP: 'Northern Mariana Islands'
NO: 'Norway'
OM: 'Oman'
PK: 'Pakistan'
PW: 'Palau'
PS: 'Palestinian Territory, Occupied'
PA: 'Panama'
PG: 'Papua New Guinea'
PY: 'Paraguay'
PE: 'Peru'
PH: 'Philippines'
PN: 'Pitcairn'
PL: 'Poland'
PT: 'Portugal'
PR: 'Puerto Rico'
QA: 'Qatar'
RE: 'Reunion'
RO: 'Romania'
RU: 'Russian Federation'
RW: 'Rwanda'
SH: 'Saint Helena'
KN: 'Saint Kitts And Nevis'
LC: 'Saint Lucia'
PM: 'Saint Pierre And Miquelon'
VC: 'Saint Vincent And The Grenadines'
WS: 'Samoa'
SM: 'San Marino'
ST: 'Sao Tome And Principe'
SA: 'Saudi Arabia'
SN: 'Senegal'
RS: 'Serbia'
SC: 'Seychelles'
SL: 'Sierra Leone'
SG: 'Singapore'
SK: 'Slovakia'
SI: 'Slovenia'
SB: 'Solomon Islands'
SO: 'Somalia'
ZA: 'South Africa'
GS: 'South Georgia And The South Sandwich Islands'
ES: 'Spain'
LK: 'Sri Lanka'
SD: 'Sudan'
SR: 'Suriname'
SJ: 'Svalbard And Jan Mayen'
SZ: 'Swaziland'
SE: 'Sweden'
CH: 'Switzerland'
SY: 'Syrian Arab Republic'
TW: 'Taiwan, Province Of China'
TJ: 'Tajikistan'
TZ: 'Tanzania, United Republic Of'
TH: 'Thailand'
TG: 'Togo'
TK: 'Tokelau'
TO: 'Tonga'
TT: 'Trinidad And Tobago'
TN: 'Tunisia'
TR: 'Turkey'
TM: 'Turkmenistan'
TC: 'Turks And Caicos Islands'
TV: 'Tuvalu'
UG: 'Uganda'
UA: 'Ukraine'
AE: 'United Arab Emirates'
GB: 'United Kingdom'
US: 'United States'
UM: 'United States Minor Outlying Islands'
UY: 'Uruguay'
UZ: 'Uzbekistan'
VU: 'Vanuatu'
VE: 'Venezuela'
VN: 'Viet Nam'
VG: 'Virgin Islands, British'
VI: 'Virgin Islands, U.s.'
WF: 'Wallis And Futuna'
EH: 'Western Sahara'
YE: 'Yemen'
ZM: 'Zambia'
ZW: 'Zimbabwe'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
#{ name: 'public', display: 'Public', tag: 'boolean', default: true },
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
{ name: 'block_ip', display: 'Blocked IPs (separated by ;)', tag: 'input', default: '', null: true },
{ name: 'block_country', display: 'Blocked countries', tag: 'column_select', multiple: true, null: true, default: '', options: @countries, seperator: ';' },
{ name: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
]

View file

@ -0,0 +1,32 @@
class App.ChatSession extends App.Model
@configure 'ChatSession', 'name', 'note'
@extend Spine.Model.Ajax
@url: @apiPath + '/chat_sessions'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false }
{ name: 'state', display: 'State', readonly: 1 }
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }
]
@configure_overview = [
'name',
'state',
'created_at',
]
uiUrl: ->
"#customer_chat/session/#{@id}"
searchResultAttributes: ->
displayName = ''
if !_.isEmpty(@name)
displayName = @displayName()
display: "##{@id} #{displayName}"
id: @id
class: 'chat_session chat_session-popover'
url: @uiUrl()
icon: 'chat'

View file

@ -1,5 +1,5 @@
class App.Overview extends App.Model
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'order', 'group_by', 'active', 'updated_at'
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'active'
@extend Spine.Model.Ajax
@url: @apiPath + '/overviews'
@configure_attributes = [

View file

@ -200,6 +200,26 @@ class App.Ticket extends App.Model
result = true if objectValue.toString().match(contains_regex)
else if condition.operator == 'contains not'
result = true if !objectValue.toString().match(contains_regex)
else if condition.operator == 'contains all'
result = true
for loopConditionValue in conditionValue
if !_.contains(objectValue, loopConditionValue)
result = false
else if condition.operator == 'contains one'
result = false
for loopConditionValue in conditionValue
if _.contains(objectValue, loopConditionValue)
result = true
else if condition.operator == 'contains all not'
result = true
for loopObjectValue in objectValue
if _.contains(conditionValue, loopObjectValue)
result = false
else if condition.operator == 'contains one not'
result = false
for loopObjectValue in objectValue
if !_.contains(conditionValue, loopObjectValue)
result = true
else if condition.operator == 'is'
result = true if objectValue.toString().trim().toLowerCase() is loopConditionValue.toString().trim().toLowerCase()
else if condition.operator == 'is not'
@ -224,3 +244,19 @@ class App.Ticket extends App.Model
throw "Unknown operator: #{condition.operator}"
result
editable: (permission = 'change') ->
user_id = App.Session.get('id')
return true if user_id is @customer_id
group_ids = App.Session.get('group_ids')
if group_ids
return true if group_ids[@group_id] && (_.include(group_ids[@group_id], permission) || _.include(group_ids[@group_id], 'full'))
role_ids = App.Session.get('role_ids')
if role_ids
for role_id in role_ids
if App.Role.exists(role_id)
role = App.Role.find(role_id)
if role.group_ids
return true if role.group_ids[@group_id] && (_.include(role.group_ids[@group_id], permission) || _.include(role.group_ids[@group_id], 'full'))
false

View file

@ -3,28 +3,31 @@
<div class="newTicket">
<div class="box box--newTicket">
<div class="page-header">
<h1><%- @T( @head ) %></h1>
<h1><%- @T(@head) %></h1>
</div>
<div class="page-content">
<ul class="tabs type-tabs">
<li class="tab u-textTruncate" data-type="phone-in">
<%- @Icon('received-calls', 'tab-icon') %>
<%- @T('Received Call') %>
</li>
<li class="tab u-textTruncate" data-type="phone-out">
<%- @Icon('outbound-calls', 'tab-icon') %>
<%- @T('Outbound Call') %>
</li>
<li class="tab u-textTruncate" data-type="email-out">
<li class="tab u-textTruncate" data-type="email-out">
<%- @Icon('email', 'tab-icon') %>
<%- @T('Send Email') %>
</li>
</ul>
<% if !_.isEmpty(@C('ui_ticket_create_notes')): %>
<% for type, note of @C('ui_ticket_create_notes'): %>
<div class="alert alert--warning js-note" role="alert" data-type="<%= type %>"><%- @T(note) %></div>
<% end %>
<% end %>
<form role="form" class="ticket-create">
<input type="hidden" name="formSenderType"/>
<input type="hidden" name="form_id" value="<%= @form_id %>"/>
@ -46,7 +49,6 @@
</div>
</div>
</div>
<div class="tabsSidebar vertical"></div>
</div>
<!--

View file

@ -132,7 +132,7 @@
<p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p>
<pre><code class="language-html js-paramsBlock">&lt;button id="feedback-form"&gt;Feedback&lt;/button&gt;
<pre class="js-modal"><code class="language-html js-code js-paramsBlock">&lt;button id="feedback-form"&gt;Feedback&lt;/button&gt;
&lt;script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"&gt;&lt;/script&gt;
@ -144,3 +144,16 @@ $(function() {
});
&lt;/script&gt;</code></pre>
</div>
<pre class="js-inlineForm"><code class="language-html js-code js-paramsBlock">&lt;div id="feedback-form"&gt;form will be placed in here&lt;/div&gt;
&lt;script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"&gt;&lt;/script&gt;
&lt;script&gt;
$(function() {
$('#feedback-form').ZammadForm({
<span class="js-modal-params"></span>
});
});
&lt;/script&gt;</code></pre>
</div>

View file

@ -1,5 +1,3 @@
<!--
<div class="chat-footer">
<div class="btn btn--primary js-createTicket">Turn chat into ticket</div>
</div>
-->
<div class="btn btn--primary js-createTicket"><%- @T('Turn chat into ticket') %></div>
</div>

View file

@ -7,9 +7,7 @@
</div>
</div>
<div class="chat-name">
<%= @name %> <div class="status-badge js-info">
<div class="info-badge"><%- @Icon('info') %></div>
</div>
<span class="js-name js-info u-clickable"><%= @name %><span> #<%= @session.id %>
</div>
<div class="chat-disconnect js-disconnect">
<div class="btn btn--action btn--small"><%- @T('disconnect') %></div>
@ -24,6 +22,25 @@
</div>
<div class="chat-body-holder js-scrollHolder">
<div class="chat-body js-body"></div>
<div class="chat-body js-meta hidden">
<% if @session: %>
<ul>
<li><%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %></li>
<% if @session && @session.preferences: %>
<% if @session.preferences.geo_ip: %>
<li>GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %></li>
<% end %>
<% if @session.preferences.remote_ip: %>
<li>IP: <%= @session.preferences.remote_ip %></li>
<% end %>
<% if @session.preferences.dns_name: %>
<li>DNS: <%= @session.preferences.dns_name %></li>
<% end %>
<% end %>
</ul>
<% end %>
<form class="js-metaForm" style="max-width: 200px; width: 100%;"></form>
</div>
</div>
<div class="chat-controls">
<div class="chat-input">

View file

@ -1,17 +0,0 @@
<hr>
<ul>
<% if @session: %>
<li><%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %>
<% end %>
<% if @session && @session.preferences: %>
<% if @session.preferences.geo_ip: %>
<li>GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %>
<% end %>
<% if @session.preferences.remote_ip: %>
<li>IP: <%= @session.preferences.remote_ip %>
<% end %>
<% if @session.preferences.dns_name: %>
<li>DNS: <%= @session.preferences.dns_name %>
<% end %>
<% end %>
</ul>

View file

@ -1,7 +1,7 @@
<div class="attachment">
<div class="attachment-name"><%= @fileName %></div>
<div class="attachment-size"><%= @fileSize %></div>
<div class="attachment-delete js-delete" data-id="<%= @store_id %>">
<div class="attachment-name"><%= @filename %></div>
<div class="attachment-size"><%= @humanFileSize(@size) %></div>
<div class="attachment-delete js-delete" data-id="<%= @id %>">
<%- @Icon('diagonal-cross') %><%- @T('Delete File') %>
</div>
</div>

View file

@ -1,3 +1,6 @@
<% if @attribute.seperator: %>
<input class="js-shadow hide" id="<%= @attribute.id %>" name="<%= @attribute.name %>" value="<%= @attribute.value %>">
<% else: %>
<select
class="columnSelect-shadow js-shadow"
id="<%= @attribute.id %>"
@ -11,6 +14,7 @@
<option value="<%= option.value %>" <%= ' selected' if option.selected %>><%= option.name %></option>
<% end %>
</select>
<% end %>
<div class="columnSelect-column columnSelect-column--selected js-selected" data-name="<%= @attribute.name %>">
<div class="u-placeholder u-unselectable js-placeholder<%= ' is-hidden' if @values.length %>"><%- @T('Nothing selected') %></div>
<% for option in @attribute.options: %>

View file

@ -1,7 +1,7 @@
<% for item in @items: %>
<div class="sidebar bottom-form-shadow flex hide" data-tab="<%= item.name %>">
<div class="sidebar-header">
<h2 class="sidebar-header-headline js-headline"><%- @T(item.head) %></h2>
<h2 class="sidebar-header-headline js-headline"><%- @T(item.sidebarHead) %></h2>
<div class="sidebar-header-actions js-actions"></div>
<div class="tabsSidebar-close">
<%- @Icon('long-arrow-right') %>
@ -13,8 +13,6 @@
<% end %>
<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) %>
</div>
<div class="tabsSidebar-tab" data-tab="<%= item.name %>"></div>
<% end %>
</div>

View file

@ -0,0 +1,4 @@
<% if @counterPossible is true: %>
<div class="tabsSidebar-tab-count js-tabCounter <% if !@counter || @counter is 0: %>hide<% end %><% if @cssClass: %><%= @cssClass %><% end %>"><%= @counter %></div>
<% end %>
<%- @Icon(@icon) %>

View file

@ -1,5 +1,5 @@
<div class="js-pager"></div>
<table class="table table-hover<%- " #{@class}" if @class %>">
<table class="table table-hover<% if @class: %> <%= @class %><% end %>">
<thead>
<tr>
<% if @sortable: %>
@ -19,14 +19,10 @@
<th style="width: 40px" class="table-radio"></th>
<% end %>
<% for header, i in @headers: %>
<th class="js-tableHead<%= " #{ header.className }" if header.className %><%= " align-#{ header.align }" if header.align %>" style="<% if header.displayWidth: %>width:<%= header.displayWidth %>px<% end %>" data-column-key="<%= header.name %>">
<th class="js-tableHead<% if header.className: %> <%= header.className %><% end %><% if header.align: %> align-<%= header.align %><% end %>" style="<% if header.displayWidth: %>width:<%= header.displayWidth %>px<% end %>" data-column-key="<%= header.name %>">
<div class="table-column-head<%= ' js-sort' if @tableId %>">
<div class="table-column-title"><%- @T(header.display) %></div>
<div class="table-column-sortIcon">
<% if header.sortOrderIcon: %>
<%- @Icon(header.sortOrderIcon[0], header.sortOrderIcon[1]) %>
<% end %>
</div>
<div class="table-column-sortIcon"><% if header.sortOrderIcon: %><%- @Icon(header.sortOrderIcon[0], header.sortOrderIcon[1]) %><% end %></div>
</div>
<% if @tableId && !header.unresizable && i < @headers.length - 1: %>
<div class="table-col-resize js-col-resize"></div>

View file

@ -21,7 +21,7 @@
</td>
<% end %>
<% for header in @headers: %>
<% value = @P(@object, header.name, @attributes) %>
<% value = @P(@object, header.name, @attributes, true) %>
<% if @callbacks: %>
<% for attribute, callbacksAll of @callbacks: %>
<% if attribute is header.name: %>
@ -31,7 +31,7 @@
<% end %>
<% end %>
<% end %>
<td<%- " class='#{ header.parentClass }'" if header.parentClass %><%- " title='#{ header.title }'" if header.title %><%- " style='text-align:#{ header.align }'" if header.align %>>
<td<% if header.parentClass: %> class="<%= header.parentClass %>"<% end %><% if header.title: %> title="<%= header.title %>"<% end %><% if header.align: %> style="text-align:<%= header.align %>"<% end %>>
<% if header.name is 'icon': %>
<%- @Icon('task-state', header.class) %>
<% else if header.icon: %>

View file

@ -7,9 +7,10 @@
<p>
<%- @T('Download and install the %s Migration Plugin on your %s instance.', 'OTRS', 'OTRS') %>:
</p>
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/617/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 5') %></a>
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/383/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 4') %></a>
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/287/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 3.3-3.1') %></a>
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/1085/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 6') %></a>
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/617/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 5') %></a>
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/383/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 4') %></a>
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/287/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 3.3-3.1') %></a>
</div>
<div class="wizard-controls horizontal center">
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>

View file

@ -64,7 +64,7 @@
<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">
<tr class="js-groups">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Groups') %></span>
<td class="progressTable-progressCell">
@ -73,7 +73,7 @@
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-organization">
<tr class="js-organizations">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Organizations') %></span>
<td class="progressTable-progressCell">
@ -82,7 +82,7 @@
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-user">
<tr class="js-users">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Users') %></span>
<td class="progressTable-progressCell">
@ -91,7 +91,7 @@
<%- @Icon('checkmark') %>
</div>
</tr>
<tr class="js-ticket">
<tr class="js-tickets">
<td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Tickets') %></span>
<td class="progressTable-progressCell">

View file

@ -20,18 +20,18 @@
<% 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: %>
<% else if @job.result && !@job.result.sum: %>
<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>
<progress max="<%= @job.result.total %>" value="<%= @job.result.sum %>"></progress>
</div>
<% end %>
<% end %>
<% if !_.isEmpty(@job.result) && @countDone: %>
<% if !_.isEmpty(@job.result) && @job.result.sum: %>
<ul>
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @job.result.sum %>/<%= @job.result.total %>):
<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>

View file

@ -1,5 +1,5 @@
<ul>
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>):
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @job.result.total %>):
<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>

View file

@ -5,19 +5,17 @@
<table class="table">
<thead>
<th style="width: 30px"></th>
<th style="width: 50px"><%- @T('ID') %></th>
<th style="width: 100px"><%- @T('ID') %></th>
<th><%- @T('Name') %></th>
<th><%- @T('Status') %></th>
<th><%- @T('Link') %></th>
<th style="width: 100px;"><%- @T('Status') %></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 title="<%= item.id %>"><%= item.id %></td>
<td title="<%= item.title %>"><a href="<%- item.link %>" target="_blank"><%= item.title %></a></td>
<td><%= item.cmdb_status_title %></td>
<td><a href="<%- item.link %>" target="_blank">i-doit</td>
</tr>
<% end %>
</tbody>

View file

@ -20,18 +20,18 @@
<% 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: %>
<% else if !@job.result.sum: %>
<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>
<progress max="<%= @job.result.total %>" value="<%= @job.result.sum %>"></progress>
</div>
<% end %>
<% end %>
<% if !_.isEmpty(@job.result) && @countDone: %>
<% if !_.isEmpty(@job.result) && @job.result.sum: %>
<ul>
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @job.result.sum %>/<%= @job.result.total %>):
<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') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
</ul>

View file

@ -1,5 +1,5 @@
<ul>
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>):
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @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') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
</ul>

View file

@ -440,7 +440,7 @@
</div>
<div class="textBubble-footer">
<div class="textBubble-signatur"><span class="js-signature">/je</span></div>
<div class="textBubble-letterCount js-letterCount">140</div>
<div class="textBubble-letterCount js-letterCount">280</div>
</div>
</div>
</div>

View file

@ -0,0 +1 @@
<a class="btn btn--action" href="<%- @downloadUrl %>" target="_blank" data-type="attachment"><%- @Icon('download') %><span><%- @T('Download %s record(s)', @count) %></span></a>

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