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 - 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 - 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! - Add the `log/production.log` file from your system. Attention: Make sure no confidential data is in it!
@ -8,7 +20,7 @@ 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 - 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 - 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 * * The upper textblock will be removed automatically when you submit your issue *
--> -->
@ -16,8 +28,10 @@ Note: We always do our best. Unfortunately, sometimes the requests are too much
### Infos: ### Infos:
* Used Zammad version: * Used Zammad version:
* Used Zammad installation source: (source, package, ...) * Installation method (source, package, ..):
* Operating system: * Operating system:
* Database + version:
* Elasticsearch version:
* Browser + version: * Browser + version:
@ -35,3 +49,4 @@ Note: We always do our best. Unfortunately, sometimes the requests are too much
* *
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/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_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 - rake db:drop
test:integration:es_postgresql: 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/controllers/search_controller_test.rb
- ruby -I test/ test/integration/report_test.rb - ruby -I test/ test/integration/report_test.rb
- ruby -I test/ test/controllers/form_controller_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 - rake db:drop
test:integration:zendesk_mysql: test:integration:zendesk_mysql:
@ -355,24 +357,36 @@ test:integration:zendesk_postgresql:
- ruby -I test/ test/integration/zendesk_import_test.rb - ruby -I test/ test/integration/zendesk_import_test.rb
- rake db:drop - rake db:drop
test:integration:otrs_5_mysql: test:integration:otrs_6_mysql:
stage: test stage: test
tags: tags:
- core - core
- mysql - mysql
script: script:
- export RAILS_ENV=test - 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:create
- rake db:migrate - rake db:migrate
- ruby -I test/ test/integration/otrs_import_test.rb - ruby -I test/ test/integration/otrs_import_test.rb
- rake db:drop - rake db:drop
test:integration:otrs_5_postgresql: test:integration:otrs_6_postgresql:
stage: test stage: test
tags: tags:
- core - core
- postgresql - 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: script:
- export RAILS_ENV=test - export RAILS_ENV=test
- export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator" - 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' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
Enabled: false Enabled: false
Style/SpaceInsideParens: Layout/SpaceInsideParens:
Description: 'No spaces after ( or before ).' Description: 'No spaces after ( or before ).'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
Enabled: false Enabled: false
Style/SpaceAfterMethodName: Layout/SpaceAfterMethodName:
Description: >- Description: >-
Do not put a space between a method name and the opening Do not put a space between a method name and the opening
parenthesis in a method definition. parenthesis in a method definition.
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
Enabled: false Enabled: false
Style/LeadingCommentSpace: Layout/LeadingCommentSpace:
Description: 'Comments should start with a space.' Description: 'Comments should start with a space.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
Enabled: false Enabled: false
Style/MethodCallParentheses: Style/MethodCallWithoutArgsParentheses:
Description: 'Do not use parentheses for method calls with no arguments.' Description: 'Do not use parentheses for method calls with no arguments.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
Enabled: false Enabled: false
Style/SpaceInsideBrackets: Layout/SpaceInsideBrackets:
Description: 'No spaces after [ or before ].' Description: 'No spaces after [ or before ].'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
Enabled: false Enabled: false
@ -83,19 +83,19 @@ Style/MethodDefParentheses:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
Enabled: false Enabled: false
Style/EmptyLinesAroundClassBody: Layout/EmptyLinesAroundClassBody:
Description: "Keeps track of empty lines around class bodies." Description: "Keeps track of empty lines around class bodies."
Enabled: false Enabled: false
Style/EmptyLinesAroundMethodBody: Layout/EmptyLinesAroundMethodBody:
Description: "Keeps track of empty lines around method bodies." Description: "Keeps track of empty lines around method bodies."
Enabled: false Enabled: false
Style/EmptyLinesAroundBlockBody: Layout/EmptyLinesAroundBlockBody:
Description: "Keeps track of empty lines around block bodies." Description: "Keeps track of empty lines around block bodies."
Enabled: false Enabled: false
Style/EmptyLinesAroundModuleBody: Layout/EmptyLinesAroundModuleBody:
Description: "Keeps track of empty lines around module bodies." Description: "Keeps track of empty lines around module bodies."
Enabled: false Enabled: false
@ -143,17 +143,29 @@ Rails/HasAndBelongsToMany:
# StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through' # StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through'
Enabled: false 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: Style/ClassAndModuleChildren:
Description: 'Checks style of children classes and modules.' Description: 'Checks style of children classes and modules.'
Enabled: false Enabled: false
Style/FileName: Naming/FileName:
Description: 'Use snake_case for source file names.' Description: 'Use snake_case for source file names.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
Enabled: true Enabled: true
Exclude: Exclude:
- 'script/websocket-server.rb' - 'script/websocket-server.rb'
Naming/VariableNumber:
Description: 'Use the configured style when numbering variables.'
Enabled: false
# 2.0 # 2.0
@ -184,8 +196,23 @@ Metrics/ModuleLength:
Description: 'Avoid modules longer than 100 lines of code.' Description: 'Avoid modules longer than 100 lines of code.'
Enabled: false Enabled: false
Metrics/BlockLength:
Enabled: false
Lint/RescueWithoutErrorClass:
Enabled: false
Rails/ApplicationRecord:
Enabled: false
# TODO # TODO
Rails/HasManyOrHasOneDependent:
Enabled: false
Style/DateTime:
Enabled: false
Style/Documentation: Style/Documentation:
Description: 'Document classes and non-namespace modules.' Description: 'Document classes and non-namespace modules.'
Enabled: false Enabled: false
@ -193,7 +220,7 @@ Style/Documentation:
Lint/UselessAssignment: Lint/UselessAssignment:
Enabled: false Enabled: false
Style/ExtraSpacing: Layout/ExtraSpacing:
Description: 'Do not use unnecessary spacing.' Description: 'Do not use unnecessary spacing.'
Enabled: false Enabled: false
@ -216,3 +243,13 @@ Style/NumericPredicate:
Enabled: true Enabled: true
Exclude: 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 - mysql
language: ruby language: ruby
rvm: rvm:
- 2.4.1 - 2.4.2
before_install: before_install:
- git fetch --unshallow - git fetch --unshallow
- sudo apt-get -qq update - sudo apt-get -qq update
@ -62,3 +62,4 @@ script:
after_success: 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-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-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 # Change Log
## [2.2.0](https://github.com/zammad/zammad/tree/2.2.0) (2017-xx-xx) ## [2.4.0](https://github.com/zammad/zammad/tree/2.4.0) (2018-xx-xx)
[Full Changelog](https://github.com/zammad/zammad/compare/2.1.0...2.2.0) [Full Changelog](https://github.com/zammad/zammad/compare/2.3.0...2.4.0)
**Implemented enhancements:** **Implemented enhancements:**

133
Gemfile
View file

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

View file

@ -65,22 +65,22 @@ GEM
addressable (2.5.2) addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0) public_suffix (>= 2.0.2, < 4.0)
arel (8.0.0) arel (8.0.0)
argon2 (1.1.3) argon2 (1.1.4)
ffi (~> 1.9) ffi (~> 1.9)
ffi-compiler (~> 0.1) ffi-compiler (~> 0.1)
ast (2.3.0) ast (2.3.0)
autoprefixer-rails (7.1.3) autoprefixer-rails (7.1.6)
execjs execjs
biz (1.7.0) biz (1.7.0)
clavius (~> 1.0) clavius (~> 1.0)
tzinfo tzinfo
browser (2.5.1) browser (2.5.2)
buftok (0.2.0) buftok (0.2.0)
builder (3.2.3) builder (3.2.3)
childprocess (0.7.1) childprocess (0.8.0)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
clavius (1.0.3) clavius (1.0.3)
clearbit (0.2.7) clearbit (0.2.8)
nestful (~> 1.1.0) nestful (~> 1.1.0)
coderay (1.1.2) coderay (1.1.2)
coffee-rails (4.2.2) coffee-rails (4.2.2)
@ -90,22 +90,24 @@ GEM
coffee-script-source coffee-script-source
execjs execjs
coffee-script-source (1.12.2) coffee-script-source (1.12.2)
coffeelint (1.16.0) coffeelint (1.16.1)
coffee-script coffee-script
execjs execjs
json json
composite_primary_keys (10.0.0) composite_primary_keys (10.0.1)
activerecord (~> 5.1.0) activerecord (~> 5.1.0)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
coveralls (0.8.21) coveralls (0.7.1)
json (>= 1.8, < 3) multi_json (~> 1.3)
simplecov (~> 0.14.1) rest-client
term-ansicolor (~> 1.3) simplecov (>= 0.7)
thor (~> 0.19.4) term-ansicolor
tins (~> 1.6) thor
crack (0.4.3) crack (0.4.3)
safe_yaml (~> 1.0.0) 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) delayed_job (4.1.3)
activesupport (>= 3.0, < 5.2) activesupport (>= 3.0, < 5.2)
delayed_job_active_record (4.1.2) delayed_job_active_record (4.1.2)
@ -127,15 +129,15 @@ GEM
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
equalizer (0.0.11) equalizer (0.0.11)
erubi (1.6.1) erubi (1.7.0)
eventmachine (1.2.5) eventmachine (1.2.5)
execjs (2.7.0) execjs (2.7.0)
factory_girl (4.8.0) factory_bot (4.8.2)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_girl_rails (4.8.0) factory_bot_rails (4.8.2)
factory_girl (~> 4.8.0) factory_bot (~> 4.8.2)
railties (>= 3.0.0) railties (>= 3.0.0)
faraday (0.11.0) faraday (0.12.2)
multipart-post (>= 1.2, < 3) multipart-post (>= 1.2, < 3)
faraday-http-cache (2.0.0) faraday-http-cache (2.0.0)
faraday (~> 0.8) faraday (~> 0.8)
@ -154,7 +156,7 @@ GEM
rainbow (>= 2.1) rainbow (>= 2.1)
rake (>= 10.0) rake (>= 10.0)
retriable (~> 2.1) retriable (~> 2.1)
globalid (0.4.0) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
guard (2.14.1) guard (2.14.1)
formatador (>= 0.2.4) formatador (>= 0.2.4)
@ -174,20 +176,21 @@ GEM
guard-symlink (0.1.1) guard-symlink (0.1.1)
guard guard
guard-compat (~> 1.1) guard-compat (~> 1.1)
hashdiff (0.3.6) hashdiff (0.3.7)
hashie (3.5.6) hashie (3.5.6)
htmlentities (4.3.4) htmlentities (4.3.4)
http (2.2.2) http (3.0.0)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) 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_parser.rb (~> 0.6.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (1.0.3) http-form_data (2.0.0)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
httpclient (2.8.3) httpclient (2.8.3)
i18n (0.8.6) i18n (0.9.1)
concurrent-ruby (~> 1.0)
icalendar (2.4.1) icalendar (2.4.1)
icalendar-recurrence (1.1.2) icalendar-recurrence (1.1.2)
icalendar (~> 2.0) icalendar (~> 2.0)
@ -210,25 +213,26 @@ GEM
logging (2.2.2) logging (2.2.2)
little-plugger (~> 1.1) little-plugger (~> 1.1)
multi_json (~> 1.10) multi_json (~> 1.10)
loofah (2.0.3) loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
lumberjack (1.0.12) lumberjack (1.0.12)
mail (2.6.6) mail (2.6.6)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
memoizable (0.4.2) memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2) method_source (0.9.0)
mime-types (2.99.3) mime-types (2.99.3)
mini_portile2 (2.3.0) mini_portile2 (2.3.0)
minitest (5.10.3) minitest (5.10.3)
multi_json (1.12.2) multi_json (1.12.2)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.0.0) multipart-post (2.0.0)
mysql2 (0.4.9) mysql2 (0.4.10)
naught (1.1.0) naught (1.1.0)
nenv (0.3.0) nenv (0.3.0)
nestful (1.1.1) nestful (1.1.3)
net-ldap (0.16.0) net-ldap (0.16.1)
netrc (0.11.0) netrc (0.11.0)
nio4r (2.1.0) nio4r (2.1.0)
nokogiri (1.8.1) nokogiri (1.8.1)
@ -246,7 +250,7 @@ GEM
rack (>= 1.2, < 3) rack (>= 1.2, < 3)
octokit (4.7.0) octokit (4.7.0)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.6.1) omniauth (1.7.1)
hashie (>= 3.4.6, < 3.6.0) hashie (>= 3.4.6, < 3.6.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
omniauth-facebook (4.0.0) omniauth-facebook (4.0.0)
@ -280,24 +284,24 @@ GEM
omniauth-weibo-oauth2 (0.4.5) omniauth-weibo-oauth2 (0.4.5)
omniauth (~> 1.5) omniauth (~> 1.5)
omniauth-oauth2 (>= 1.4.0) omniauth-oauth2 (>= 1.4.0)
parser (2.4.0.0) parallel (1.12.0)
ast (~> 2.2) parser (2.4.0.2)
ast (~> 2.3)
pg (0.21.0) pg (0.21.0)
pluginator (1.5.0) pluginator (1.5.0)
power_assert (1.1.0) power_assert (1.1.1)
powerpack (0.1.1) powerpack (0.1.1)
pre-commit (0.35.0) pre-commit (0.37.0)
pluginator (~> 1.5) pluginator (~> 1.5)
pry (0.10.4) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.9.0)
slop (~> 3.4) public_suffix (3.0.1)
public_suffix (3.0.0) puma (3.11.0)
puma (3.10.0)
rack (2.0.3) rack (2.0.3)
rack-livereload (0.3.16) rack-livereload (0.3.16)
rack rack
rack-test (0.7.0) rack-test (0.8.2)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rails (5.1.4) rails (5.1.4)
actioncable (= 5.1.4) actioncable (= 5.1.4)
@ -327,7 +331,7 @@ GEM
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.19.0) raindrops (0.19.0)
rake (12.1.0) rake (12.3.0)
rb-fsevent (0.10.2) rb-fsevent (0.10.2)
rb-inotify (0.9.10) rb-inotify (0.9.10)
ffi (>= 0.5.0, < 2) ffi (>= 0.5.0, < 2)
@ -337,39 +341,40 @@ GEM
mime-types (>= 1.16, < 3.0) mime-types (>= 1.16, < 3.0)
netrc (~> 0.7) netrc (~> 0.7)
retriable (2.1.0) retriable (2.1.0)
rspec-core (3.6.0) rspec-core (3.7.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.0)
rspec-expectations (3.6.0) rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.0)
rspec-mocks (3.6.0) rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.0)
rspec-rails (3.6.1) rspec-rails (3.7.2)
actionpack (>= 3.0) actionpack (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
railties (>= 3.0) railties (>= 3.0)
rspec-core (~> 3.6.0) rspec-core (~> 3.7.0)
rspec-expectations (~> 3.6.0) rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.6.0) rspec-mocks (~> 3.7.0)
rspec-support (~> 3.6.0) rspec-support (~> 3.7.0)
rspec-support (3.6.0) rspec-support (3.7.0)
rubocop (0.42.0) rubocop (0.51.0)
parser (>= 2.3.1.1, < 3.0) parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.8.1) ruby-progressbar (1.9.0)
ruby_dep (1.5.0) ruby_dep (1.5.0)
rubyzip (1.2.1) rubyzip (1.2.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sass (3.5.1) sass (3.5.3)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.7)
sass-rails (5.0.6) sass-rails (5.0.7)
railties (>= 4.0.0, < 6) railties (>= 4.0.0, < 6)
sass (~> 3.1) sass (~> 3.1)
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
@ -383,9 +388,8 @@ GEM
rubyzip (~> 1.0) rubyzip (~> 1.0)
websocket (~> 1.0) websocket (~> 1.0)
shellany (0.0.1) shellany (0.0.1)
simple-rss (1.3.1)
simple_oauth (0.3.1) simple_oauth (0.3.1)
simplecov (0.14.1) simplecov (0.15.1)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
simplecov-html (~> 0.10.0) simplecov-html (~> 0.10.0)
@ -393,11 +397,6 @@ GEM
simplecov-rcov (0.2.3) simplecov-rcov (0.2.3)
simplecov (>= 0.4.1) simplecov (>= 0.4.1)
slack-notifier (2.3.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) sprockets (3.7.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
@ -410,26 +409,27 @@ GEM
rest-client (~> 1.7, >= 1.7.3) rest-client (~> 1.7, >= 1.7.3)
term-ansicolor (1.6.0) term-ansicolor (1.6.0)
tins (~> 1.0) tins (~> 1.0)
test-unit (3.2.5) test-unit (3.2.6)
power_assert power_assert
therubyracer (0.12.3) therubyracer (0.12.3)
libv8 (~> 3.16.14.15) libv8 (~> 3.16.14.15)
ref ref
thor (0.19.4) thor (0.20.0)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8) tilt (2.0.8)
tins (1.15.0) tins (1.15.1)
twitter (6.1.0) twitter (6.2.0)
addressable (~> 2.5) addressable (~> 2.3)
buftok (~> 0.2.0) buftok (~> 0.2.0)
equalizer (= 0.0.11) equalizer (~> 0.0.11)
faraday (~> 0.11.0) http (~> 3.0)
http (~> 2.1) http-form_data (~> 2.0)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
memoizable (~> 0.4.2) memoizable (~> 0.4.0)
naught (~> 1.1) multipart-post (~> 2.0)
simple_oauth (~> 0.3.1) naught (~> 1.0)
tzinfo (1.2.3) simple_oauth (~> 0.3.0)
tzinfo (1.2.4)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (3.2.0) uglifier (3.2.0)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
@ -437,10 +437,10 @@ GEM
unf_ext unf_ext
unf_ext (0.0.7.4) unf_ext (0.0.7.4)
unicode-display_width (1.3.0) unicode-display_width (1.3.0)
unicorn (5.3.0) unicorn (5.3.1)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
valid_email2 (2.0.1) valid_email2 (2.1.0)
activemodel (>= 3.2) activemodel (>= 3.2)
mail (~> 2.5) mail (~> 2.5)
viewpoint (1.1.0) viewpoint (1.1.0)
@ -448,16 +448,16 @@ GEM
logging logging
nokogiri nokogiri
rubyntlm rubyntlm
webmock (3.0.1) webmock (3.1.1)
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
websocket (1.2.4) websocket (1.2.5)
websocket-driver (0.6.5) websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2) websocket-extensions (0.1.3)
writeexcel (1.0.5) writeexcel (1.0.5)
zendesk_api (1.14.4) zendesk_api (1.16.0)
faraday (~> 0.9) faraday (~> 0.9)
hashie (>= 3.5.2, < 4.0.0) hashie (>= 3.5.2, < 4.0.0)
inflection inflection
@ -482,6 +482,7 @@ DEPENDENCIES
composite_primary_keys composite_primary_keys
coveralls coveralls
daemons daemons
dalli
delayed_job_active_record delayed_job_active_record
diffy diffy
doorkeeper doorkeeper
@ -489,7 +490,7 @@ DEPENDENCIES
em-websocket em-websocket
eventmachine eventmachine
execjs execjs
factory_girl_rails factory_bot_rails
figaro figaro
github_changelog_generator github_changelog_generator
guard guard
@ -501,7 +502,7 @@ DEPENDENCIES
json json
koala koala
libv8 libv8
mail mail (= 2.6.6)
mime-types mime-types
mysql2 mysql2
net-ldap net-ldap
@ -528,12 +529,9 @@ DEPENDENCIES
rubyntlm! rubyntlm!
sass-rails sass-rails
selenium-webdriver (= 2.53.4) selenium-webdriver (= 2.53.4)
simple-rss
simplecov simplecov
simplecov-rcov simplecov-rcov
slack-notifier slack-notifier
spring
spring-commands-rspec
sprockets sprockets
sqlite3 sqlite3
telegramAPI telegramAPI
@ -549,7 +547,7 @@ DEPENDENCIES
zendesk_api zendesk_api
RUBY VERSION RUBY VERSION
ruby 2.4.1p111 ruby 2.4.2p198
BUNDLED WITH 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 Zammad is a web based open source helpdesk/customer support system with many
features to manage customer communication via several channels like telephone, features to manage customer communication via several channels like telephone,
facebook, twitter, chat and e-mails. It is distributed under the GNU AFFERO facebook, twitter, chat and e-mails. It is distributed under the GNU AFFERO
General Public License (AGPL) and tested on Linux, Solaris, AIX, FreeBSD, General Public License (AGPL).
OpenBSD and Mac OS 10.x. Do you receive many e-mails and want to answer them
with a team of agents? Do you receive many e-mails and want to answer them with a team of agents?
You're going to love Zammad! 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: '' headPrefix: ''
shown: true shown: true
closeOnAnyClick: false closeOnAnyClick: false
initalFormParams: {}
initalFormParamsIgnore: false
events: events:
'submit form': 'submit' 'submit form': 'submit'
@ -746,10 +748,10 @@ class App.ControllerModal extends App.Controller
centerButtons: @centerButtons centerButtons: @centerButtons
leftButtons: @leftButtons leftButtons: @leftButtons
) )
modal.find('.modal-body').html content modal.find('.modal-body').html(content)
if !@initRenderingDone if !@initRenderingDone
@initRenderingDone = true @initRenderingDone = true
@html modal @html(modal)
else else
@$('.modal-dialog').replaceWith(modal) @$('.modal-dialog').replaceWith(modal)
@post() @post()
@ -761,6 +763,8 @@ class App.ControllerModal extends App.Controller
@el @el
render: => render: =>
@initalFormParamsIgnore = false
if @buttonSubmit is true if @buttonSubmit is true
@buttonSubmit = 'Submit' @buttonSubmit = 'Submit'
if @buttonCancel is true if @buttonCancel is true
@ -775,19 +779,18 @@ class App.ControllerModal extends App.Controller
if @small if @small
@el.addClass('modal--small') @el.addClass('modal--small')
@el.modal @el.modal(
keyboard: @keyboard keyboard: @keyboard
show: true show: true
backdrop: @backdrop backdrop: @backdrop
container: @container container: @container
.on ).on(
'show.bs.modal': @onShow 'show.bs.modal': @localOnShow
'shown.bs.modal': @onShown 'shown.bs.modal': @localOnShown
'hide.bs.modal': @onClose 'hide.bs.modal': @localOnClose
'hidden.bs.modal': => 'hidden.bs.modal': @localOnClosed
@onClosed() 'dismiss.bs.modal': @localOnCancel
$('.modal').remove() )
'dismiss.bs.modal': @onCancel
if @closeOnAnyClick if @closeOnAnyClick
@el.on('click', => @el.on('click', =>
@ -797,6 +800,7 @@ class App.ControllerModal extends App.Controller
close: (e) => close: (e) =>
if e if e
e.preventDefault() e.preventDefault()
@initalFormParamsIgnore = true
@el.modal('hide') @el.modal('hide')
formParams: => formParams: =>
@ -804,28 +808,50 @@ class App.ControllerModal extends App.Controller
return @formParam(@container.find('.modal form')) return @formParam(@container.find('.modal form'))
return @formParam(@$('.modal form')) return @formParam(@$('.modal form'))
onShow: -> localOnShow: (e) =>
@onShow(e)
onShow: (e) ->
# do nothing # do nothing
onShown: => localOnShown: (e) =>
@onShown(e)
onShown: (e) =>
@$('input:not([disabled]):not([type="hidden"]):not(".btn"), textarea').first().focus() @$('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: -> onClose: ->
# do nothing # do nothing
onClosed: -> localOnClosed: (e) =>
@onClosed(e)
$('.modal').remove()
onClosed: (e) ->
# do nothing # do nothing
onSubmit: -> localOnCancel: (e) =>
# do nothing @onCancel(e)
onCancel: -> onCancel: (e) ->
# do nothing # do nothing
cancel: (e) => cancel: (e) =>
@close(e) @close(e)
@onCancel(e) @onCancel(e)
onSubmit: (e) ->
# do nothing
submit: (e) => submit: (e) =>
e.stopPropagation() e.stopPropagation()
e.preventDefault() e.preventDefault()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -131,13 +131,12 @@ class Form extends App.Controller
if _.isEmpty(job) if _.isEmpty(job)
@lastImport.html('') @lastImport.html('')
return return
countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped + job.result.failed
if !job.result.roles if !job.result.roles
job.result.roles = {} job.result.roles = {}
for role_id, statistic of job.result.role_ids for role_id, statistic of job.result.role_ids
role = App.Role.find(role_id) role = App.Role.find(role_id)
job.result.roles[role.displayName()] = statistic 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) @lastImport.html(el)
activeDryRun: => activeDryRun: =>
@ -540,34 +539,26 @@ class ConnectionWizard extends App.WizardModal
@showAlert('js-error', (job.result.error || job.result.info)) @showAlert('js-error', (job.result.error || job.result.info))
return return
total = 0
if job.result && _.keys(job.result).length > 0 if job.result && _.keys(job.result).length > 0
@$('.js-preprogress').addClass('hide') @$('.js-preprogress').addClass('hide')
@$('.js-analyzing').removeClass('hide') @$('.js-analyzing').removeClass('hide')
analized = 0 @$('.js-progress progress').attr('value', job.result.sum)
total = job.result.sum @$('.js-progress progress').attr('max', job.result.total)
for action, sum of job.result
continue if action == 'folders'
continue if action == 'sum'
analized += sum
@$('.js-progress progress').attr('value', analized)
@$('.js-progress progress').attr('max', total)
if job.finished_at if job.finished_at
# reset initial state in case the back button is used # reset initial state in case the back button is used
@$('.js-preprogress').removeClass('hide') @$('.js-preprogress').removeClass('hide')
@$('.js-analyzing').addClass('hide') @$('.js-analyzing').addClass('hide')
@tryResult(job, total) @tryResult(job)
else else
@delay(@tryLoop, 4000) @delay(@tryLoop, 4000)
) )
tryResult: (job, total) => tryResult: (job) =>
@showSlide('js-try') @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) @el.find('.js-summary').html(el)
App.Config.set( App.Config.set(

View file

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

View file

@ -32,7 +32,7 @@ class App.SettingsAreaItem extends App.Controller
) )
new App.ControllerForm( new App.ControllerForm(
el: @el.find('.form-item'), el: @el.find('.form-item')
model: { configure_attributes: @configure_attributes, className: '' } model: { configure_attributes: @configure_attributes, className: '' }
autofocus: false 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')}" number = "#{App.Config.get('ticket_hook')}#{App.Config.get('system_id')}"
counter = '1' counter = '1'
if paramsItem.min_size if paramsItem.min_size
minSize = parseInt(paramsItem.min_size) minSize = parseInt(paramsItem.min_size) - "#{App.Config.get('system_id')}".length
if paramsItem.checksum if paramsItem.checksum
minSize -= 1 minSize -= 1
if minSize > 1 if minSize > 1

View file

@ -6,24 +6,24 @@ class App.UiElement.column_select extends App.UiElement.ApplicationUiElement
attribute.multiple = 'multiple' attribute.multiple = 'multiple'
# build options list based on config # build options list based on config
@getConfigOptionList( attribute, params ) @getConfigOptionList(attribute, params)
# build options list based on relation # build options list based on relation
@getRelationOptionList( attribute, params ) @getRelationOptionList(attribute, params)
# add null selection if needed # add null selection if needed
@addNullOption( attribute, params ) @addNullOption(attribute, params)
# sort attribute.options # sort attribute.options
@sortOptions( attribute, params ) @sortOptions(attribute, params)
# find selected/checked item of list # find selected/checked item of list
@selectedOptions( attribute, params ) @selectedOptions(attribute, params)
# disable item of list # disable item of list
@disabledOptions( attribute, params ) @disabledOptions(attribute, params)
# filter attributes # 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 # coffeelint: disable=camel_case_classes
class App.UiElement.richtext class App.UiElement.richtext
@render: (attribute) -> @render: (attribute, params) ->
item = $( App.view('generic/richtext')(attribute: attribute) )
item = $( App.view('generic/richtext')( attribute: attribute ) )
item.find('[contenteditable]').ce( item.find('[contenteditable]').ce(
mode: attribute.type mode: attribute.type
maxlength: attribute.maxlength maxlength: attribute.maxlength
@ -15,42 +14,42 @@ class App.UiElement.richtext
new App[plugin.controller](params) new App[plugin.controller](params)
if attribute.upload if attribute.upload
item.append( $( App.view('generic/attachment')( attribute: attribute ) ) ) @attachments = []
item.append( $( App.view('generic/attachment')(attribute: attribute) ) )
renderAttachment = (file) => renderFile = (file) =>
item.find('.attachments').append( App.view('generic/attachment_item')( item.find('.attachments').append(App.view('generic/attachment_item')(file))
fileName: file.filename @attachments.push file
fileSize: App.Utils.humanFileSize(file.size)
store_id: file.store_id if params && params.attachments
)) for file in params.attachments
item.on( renderFile(file)
'click'
"[data-id=#{file.store_id}]", (e) => # remove items
item.find('.attachments').on('click', '.js-delete', (e) =>
id = $(e.currentTarget).data('id')
@attachments = _.filter( @attachments = _.filter(
@attachments, @attachments,
(item) -> (item) ->
return if item.id isnt file.store_id return if item.id.toString() is id.toString()
item item
) )
store_id = $(e.currentTarget).data('id')
# delete attachment from storage # delete attachment from storage
App.Ajax.request( App.Ajax.request(
type: 'DELETE' type: 'DELETE'
url: "#{App.Config.get('api_path')}/ticket_attachment_upload" url: "#{App.Config.get('api_path')}/ticket_attachment_upload"
data: JSON.stringify(store_id: store_id), data: JSON.stringify(id: id),
processData: false processData: false
) )
# remove attachment from dom # remove attachment from dom
element = $(e.currentTarget).closest('.attachments') element = $(e.currentTarget).closest('.attachments')
$(e.currentTarget).closest('.attachment').remove() $(e.currentTarget).closest('.attachment').remove()
# empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o
if element.find('.attachment').length == 0 if element.find('.attachment').length == 0
element.empty() element.empty()
) )
@attachments = []
@progressBar = item.find('.attachmentUpload-progressBar') @progressBar = item.find('.attachmentUpload-progressBar')
@progressText = item.find('.js-percentage') @progressText = item.find('.js-percentage')
@attachmentPlaceholder = item.find('.attachmentPlaceholder') @attachmentPlaceholder = item.find('.attachmentPlaceholder')
@ -84,7 +83,6 @@ class App.UiElement.richtext
# Called after received response from the server # Called after received response from the server
onCompleted: (response) => onCompleted: (response) =>
response = JSON.parse(response) response = JSON.parse(response)
@attachments.push response.data
@attachmentPlaceholder.removeClass('hide') @attachmentPlaceholder.removeClass('hide')
@attachmentUpload.addClass('hide') @attachmentUpload.addClass('hide')
@ -93,7 +91,7 @@ class App.UiElement.richtext
@progressBar.width(parseInt(0) + '%') @progressBar.width(parseInt(0) + '%')
@progressText.text('') @progressText.text('')
renderAttachment(response.data) renderFile(response.data)
item.find('input').val('') item.find('input').val('')
App.Log.debug 'UiElement.richtext', 'upload complete', response.data 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') App.Delay.set(u, 100, undefined, 'form_upload')
item 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)'] '^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)'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
'boolean$': ['is', 'is not'] 'boolean$': ['is', 'is not']
'integer$': ['is', 'is not']
'^radio$': ['is', 'is not'] '^radio$': ['is', 'is not']
'^select$': ['is', 'is not'] '^select$': ['is', 'is not']
'^tree_select$': ['is', 'is not']
'^input$': ['contains', 'contains not'] '^input$': ['contains', 'contains not']
'^richtext$': ['contains', 'contains not']
'^textarea$': ['contains', 'contains not'] '^textarea$': ['contains', 'contains not']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one 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'] '^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'] '^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed']
'boolean$': ['is', 'is not', 'has changed'] 'boolean$': ['is', 'is not', 'has changed']
'integer$': ['is', 'is not', 'has changed']
'^radio$': ['is', 'is not', 'has changed'] '^radio$': ['is', 'is not', 'has changed']
'^select$': ['is', 'is not', 'has changed'] '^select$': ['is', 'is not', 'has changed']
'^tree_select$': ['is', 'is not', 'has changed']
'^input$': ['contains', 'contains not', 'has changed'] '^input$': ['contains', 'contains not', 'has changed']
'^richtext$': ['contains', 'contains not', 'has changed']
'^textarea$': ['contains', 'contains not', 'has changed'] '^textarea$': ['contains', 'contains not', 'has changed']
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not'] '^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 # define default type
@default_type = 'phone-in' @default_type = 'phone-in'
@formId = App.ControllerForm.formId()
# remember split info if exists # remember split info if exists
@split = '' @split = ''
if @ticket_id && @article_id if @ticket_id && @article_id
@ -92,6 +94,10 @@ class App.TicketCreate extends App.Controller
else else
@$('[name="cc"]').closest('.form-group').addClass('hide') @$('[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) App.TaskManager.touch(@task_key)
meta: => meta: =>
@ -158,7 +164,7 @@ class App.TicketCreate extends App.Controller
# get data / in case also ticket data for split # get data / in case also ticket data for split
buildScreen: (params) => buildScreen: (params) =>
if !params.ticket_id && !params.article_id if _.isEmpty(params.ticket_id) && _.isEmpty(params.article_id)
if !_.isEmpty(params.customer_id) if !_.isEmpty(params.customer_id)
@render(options: { customer_id: params.customer_id }) @render(options: { customer_id: params.customer_id })
return return
@ -173,6 +179,7 @@ class App.TicketCreate extends App.Controller
data: data:
ticket_id: params.ticket_id ticket_id: params.ticket_id
article_id: params.article_id article_id: params.article_id
form_id: @formId
processData: true processData: true
success: (data, status, xhr) => success: (data, status, xhr) =>
@ -194,6 +201,9 @@ class App.TicketCreate extends App.Controller
else else
t.body = App.Utils.text2html(a.body) t.body = App.Utils.text2html(a.body)
# add attachments
t.attachments = data.attachments
# render page # render page
@render(options: t) @render(options: t)
) )
@ -201,23 +211,20 @@ class App.TicketCreate extends App.Controller
render: (template = {}) -> render: (template = {}) ->
# get params # get params
params = {} params = @prefilledParams || {}
if template && !_.isEmpty(template.options) if template && !_.isEmpty(template.options)
params = template.options params = template.options
else if App.TaskManager.get(@task_key) && !_.isEmpty(App.TaskManager.get(@task_key).state) else if App.TaskManager.get(@task_key) && !_.isEmpty(App.TaskManager.get(@task_key).state)
params = 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'] @html(App.view('agent_ticket_create')(
@form_id = params['form_id']
else
@form_id = App.ControllerForm.formId()
@html App.view('agent_ticket_create')(
head: 'New Ticket' head: 'New Ticket'
agent: @permissionCheck('ticket.agent') agent: @permissionCheck('ticket.agent')
admin: @permissionCheck('admin') admin: @permissionCheck('admin')
form_id: @form_id form_id: @formId
) ))
signatureChanges = (params, attribute, attributes, classname, form, ui) => signatureChanges = (params, attribute, attributes, classname, form, ui) =>
if attribute && attribute.name is 'group_id' if attribute && attribute.name is 'group_id'
@ -272,7 +279,7 @@ class App.TicketCreate extends App.Controller
} }
new App.ControllerForm( new App.ControllerForm(
el: @$('.ticket-form-top') el: @$('.ticket-form-top')
form_id: @form_id form_id: @formId
model: App.Ticket model: App.Ticket
screen: 'create_top' screen: 'create_top'
events: events:
@ -288,14 +295,14 @@ class App.TicketCreate extends App.Controller
new App.ControllerForm( new App.ControllerForm(
el: @$('.article-form-top') el: @$('.article-form-top')
form_id: @form_id form_id: @formId
model: App.TicketArticle model: App.TicketArticle
screen: 'create_top' screen: 'create_top'
params: params params: params
) )
new App.ControllerForm( new App.ControllerForm(
el: @$('.ticket-form-middle') el: @$('.ticket-form-middle')
form_id: @form_id form_id: @formId
model: App.Ticket model: App.Ticket
screen: 'create_middle' screen: 'create_middle'
events: events:
@ -310,7 +317,7 @@ class App.TicketCreate extends App.Controller
) )
new App.ControllerForm( new App.ControllerForm(
el: @$('.ticket-form-bottom') el: @$('.ticket-form-bottom')
form_id: @form_id form_id: @formId
model: App.Ticket model: App.Ticket
screen: 'create_bottom' screen: 'create_bottom'
events: events:
@ -420,7 +427,7 @@ class App.TicketCreate extends App.Controller
body: params.body body: params.body
type_id: type.id type_id: type.id
sender_id: sender.id sender_id: sender.id
form_id: @form_id form_id: @formId
content_type: 'text/html' content_type: 'text/html'
} }
else else
@ -432,7 +439,7 @@ class App.TicketCreate extends App.Controller
body: params.body body: params.body
type_id: type.id type_id: type.id
sender_id: sender.id sender_id: sender.id
form_id: @form_id form_id: @formId
content_type: 'text/html' content_type: 'text/html'
} }

View file

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

View file

@ -1,26 +1,62 @@
class SidebarCustomer extends App.Controller class SidebarCustomer extends App.Controller
sidebarItem: => sidebarItem: =>
return if !@permissionCheck('ticket.agent') return if !@permissionCheck('ticket.agent')
return if !@params.customer_id return if _.isEmpty(@params.customer_id)
{ @item = {
head: 'Customer'
name: 'customer' name: 'customer'
icon: 'person' badgeCallback: @badgeRender
actions: [ sidebarHead: 'Customer'
sidebarCallback: @showCustomer
sidebarActions: [
{ {
title: 'Edit Customer' title: 'Edit Customer'
name: 'customer-edit' name: 'customer-edit'
callback: @editCustomer 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) => showCustomer: (el) =>
@el = el @elSidebar = el
return if _.isEmpty(@params.customer_id)
new App.WidgetUser( new App.WidgetUser(
el: @el el: @elSidebar
user_id: @params.customer_id user_id: @params.customer_id
callback: @badgeRenderLocal
) )
editCustomer: => editCustomer: =>
@ -32,7 +68,7 @@ class SidebarCustomer extends App.Controller
title: 'Users' title: 'Users'
object: 'User' object: 'User'
objects: 'Users' objects: 'Users'
container: @el.closest('.content') container: @elSidebar.closest('.content')
) )
App.Config.set('200-Customer', SidebarCustomer, 'TicketCreateSidebar') 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) customer = App.User.find(@params.customer_id)
@organization_id = customer.organization_id @organization_id = customer.organization_id
return if !@organization_id return if !@organization_id
{ @item = {
head: 'Organization'
name: 'organization' name: 'organization'
icon: 'group' badgeIcon: 'group'
actions: [ sidebarHead: 'Organization'
sidebarCallback: @showOrganization
sidebarActions: [
{ {
title: 'Edit Organization' title: 'Edit Organization'
name: 'organization-edit' name: 'organization-edit'
callback: @editOrganization callback: @editOrganization
}, },
] ]
callback: @showOrganization
} }
@item
showOrganization: (el) => showOrganization: (el) =>
@el = el @elSidebar = el
new App.WidgetOrganization( new App.WidgetOrganization(
el: @el el: @elSidebar
organization_id: @organization_id organization_id: @organization_id
) )
@ -35,7 +36,7 @@ class SidebarOrganization extends App.Controller
title: 'Organizations' title: 'Organizations'
object: 'Organization' object: 'Organization'
objects: 'Organizations' objects: 'Organizations'
container: @el.closest('.content') container: @elSidebar.closest('.content')
) )
App.Config.set('300-Organization', SidebarOrganization, 'TicketCreateSidebar') App.Config.set('300-Organization', SidebarOrganization, 'TicketCreateSidebar')

View file

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

View file

@ -35,7 +35,7 @@ class App.CustomerChat extends App.Controller
active_agent_ids: [] active_agent_ids: []
@render() @render()
@on 'layout-has-changed', @propagateLayoutChange @on('layout-has-changed', @propagateLayoutChange)
# update navbar on new status # update navbar on new status
@bind('chat_status_agent', (data) => @bind('chat_status_agent', (data) =>
@ -163,6 +163,12 @@ class App.CustomerChat extends App.Controller
@title 'Customer Chat', true @title 'Customer Chat', true
@navupdate '#customer_chat' @navupdate '#customer_chat'
if params.session_id
callback = (session) =>
@addChat(session)
App.ChatSession.full(params.session_id, callback)
@navigate '#customer_chat'
active: (state) => active: (state) =>
return @shown if state is undefined return @shown if state is undefined
@shown = state @shown = state
@ -264,10 +270,11 @@ class App.CustomerChat extends App.Controller
addChat: (session) -> addChat: (session) ->
return if @chatWindows[session.session_id] return if @chatWindows[session.session_id]
chat = new ChatWindow chat = new ChatWindow(
session: session session: session
removeCallback: @removeChat removeCallback: @removeChat
messageCallback: @updateNavMenu messageCallback: @updateNavMenu
)
@workspace.append chat.el @workspace.append chat.el
chat.render() chat.render()
@ -289,7 +296,7 @@ class App.CustomerChat extends App.Controller
propagateLayoutChange: (event) => propagateLayoutChange: (event) =>
# adjust scroll position on layoutChange # adjust scroll position on layoutChange
for session_id, chat of @chatWindows for session_id, chat of @chatWindows
chat.trigger 'layout-changed' chat.trigger('layout-changed')
acceptChat: => acceptChat: =>
return if @windowCount() >= @maxChatWindows return if @windowCount() >= @maxChatWindows
@ -324,19 +331,6 @@ class App.CustomerChat extends App.Controller
currentPosition: => currentPosition: =>
@$('.main').scrollTop() @$('.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 class ChatWindow extends App.Controller
className: 'chat-window' className: 'chat-window'
@ -348,6 +342,9 @@ class ChatWindow extends App.Controller
'click .js-close': 'close' 'click .js-close': 'close'
'click .js-disconnect': 'disconnect' 'click .js-disconnect': 'disconnect'
'click .js-scrollHint': 'onScrollHintClick' 'click .js-scrollHint': 'onScrollHintClick'
'click .js-info': 'toggleMeta'
'click .js-createTicket': 'ticketCreate'
'submit .js-metaForm': 'sendMetaForm'
elements: elements:
'.js-customerChatInput': 'input' '.js-customerChatInput': 'input'
@ -355,8 +352,11 @@ class ChatWindow extends App.Controller
'.js-close': 'closeButton' '.js-close': 'closeButton'
'.js-disconnect': 'disconnectButton' '.js-disconnect': 'disconnectButton'
'.js-body': 'body' '.js-body': 'body'
'.js-meta': 'meta'
'.js-name': 'metaName'
'.js-scrollHolder': 'scrollHolder' '.js-scrollHolder': 'scrollHolder'
'.js-scrollHint': 'scrollHint' '.js-scrollHint': 'scrollHint'
'.js-metaForm': 'metaForm'
sounds: sounds:
message: new Audio('assets/sounds/chat_message.mp3') message: new Audio('assets/sounds/chat_message.mp3')
@ -374,9 +374,11 @@ class ChatWindow extends App.Controller
@scrollSnapTolerance = 10 # pixels @scrollSnapTolerance = 10 # pixels
@chat = App.Chat.find(@session.chat_id) @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) => @bind('chat_session_typing', (data) =>
return if data.session_id isnt @session.session_id return if data.session_id isnt @session.session_id
@ -413,12 +415,45 @@ class ChatWindow extends App.Controller
onLayoutChange: => onLayoutChange: =>
@scrollToBottom() @scrollToBottom()
render: -> toggleMeta: =>
@html App.view('customer_chat/chat_window') if @meta.hasClass('hidden')
name: @name @showMeta()
else
@hideMeta()
@el.one 'transitionend', @onTransitionend hideMeta: =>
@scrollHolder.scroll @detectScrolledtoBottom @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 # force repaint
@el.prop('offsetHeight') @el.prop('offsetHeight')
@ -426,18 +461,24 @@ class ChatWindow extends App.Controller
# @addMessage 'Hello. My name is Roger, how can I help you?', 'agent' # @addMessage 'Hello. My name is Roger, how can I help you?', 'agent'
if @session 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 if @session && @session.preferences && @session.preferences.url
@addNoticeMessage(@session.preferences.url) @addNoticeMessage(@session.preferences.url, undefined, activeChat)
if @session.messages if @session.messages
for message in @session.messages for message in @session.messages
if message.created_by_id if message.created_by_id
@addMessage message.content, 'agent' @addMessage(message.content, 'agent', false, activeChat)
else else
@addMessage message.content, 'customer' @addMessage(message.content, 'customer', false, activeChat)
# send init reply # send init reply
if !@session.messages || _.isEmpty(@session.messages) if activeChat && _.isEmpty(@session.messages)
preferences = @Session.get('preferences') preferences = @Session.get('preferences')
if preferences.chat && preferences.chat.phrase if preferences.chat && preferences.chat.phrase
phrases = preferences.chat.phrase[@session.chat_id] phrases = preferences.chat.phrase[@session.chat_id]
@ -447,20 +488,9 @@ class ChatWindow extends App.Controller
@input.html(phrase) @input.html(phrase)
@sendMessage(1600) @sendMessage(1600)
@$('.js-info').popover( # set chat to offline if state is already closed
trigger: 'hover' if !activeChat
html: true @goOffline()
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
)
)
# show text module UI # show text module UI
new App.WidgetTextModule( new App.WidgetTextModule(
@ -470,6 +500,18 @@ class ChatWindow extends App.Controller
config: App.Config.all() 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: => focus: =>
@input.focus() @input.focus()
@ -498,7 +540,8 @@ class ChatWindow extends App.Controller
@goOffline() @goOffline()
close: => close: =>
@el.one 'transitionend', { callback: @release }, @onTransitionend @sendMetaForm()
@el.one('transitionend', { callback: @release }, @onTransitionend)
@el.removeClass('is-open') @el.removeClass('is-open')
if @removeCallback if @removeCallback
@removeCallback(@session.session_id) @removeCallback(@session.session_id)
@ -577,7 +620,8 @@ class ChatWindow extends App.Controller
) )
@delay(send, delay) @delay(send, delay)
@addMessage content, 'agent' @hideMeta()
@addMessage(content, 'agent')
@input.html('') @input.html('')
updateModified: (state) => updateModified: (state) =>
@ -614,18 +658,19 @@ class ChatWindow extends App.Controller
@messageCallback(@session.session_id) @messageCallback(@session.session_id)
@unreadMessagesCounter = 0 @unreadMessagesCounter = 0
addMessage: (message, sender, isNew) => addMessage: (message, sender, isNew, useMaybeAddTimestamp = true) =>
@maybeAddTimestamp() @maybeAddTimestamp() if useMaybeAddTimestamp
@lastAddedType = sender @lastAddedType = sender
@body.append App.view('customer_chat/chat_message') @body.append App.view('customer_chat/chat_message')(
message: message message: message
sender: sender sender: sender
isNew: isNew isNew: isNew
timestamp: Date.now() timestamp: Date.now()
)
@scrollToBottom showHint: true @scrollToBottom(showHint: true)
showWritingLoader: => showWritingLoader: =>
if !@isTyping if !@isTyping
@ -667,33 +712,37 @@ class ChatWindow extends App.Controller
@lastAddedType = 'timestamp' @lastAddedType = 'timestamp'
addTimestamp: (label, time) => addTimestamp: (label, time) =>
@body.append App.view('customer_chat/chat_timestamp') @body.append App.view('customer_chat/chat_timestamp')(
label: label label: label
time: time time: time
)
updateLastTimestamp: (label, time) -> updateLastTimestamp: (label, time) ->
@body @body
.find('.js-timestamp') .find('.js-timestamp')
.last() .last()
.replaceWith App.view('customer_chat/chat_timestamp') .replaceWith App.view('customer_chat/chat_timestamp')(
label: label label: label
time: time time: time
)
addStatusMessage: (message, args) -> addStatusMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() @maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_status_message') @body.append App.view('customer_chat/chat_status_message')(
message: message message: message
args: args args: args
)
@scrollToBottom() @scrollToBottom()
addNoticeMessage: (message, args) -> addNoticeMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() @maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_notice_message') @body.append App.view('customer_chat/chat_notice_message')(
message: message message: message
args: args args: args
)
@scrollToBottom() @scrollToBottom()
@ -717,6 +766,37 @@ class ChatWindow extends App.Controller
else if showHint else if showHint
@showScrollHint() @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 class Setting extends App.ControllerModal
buttonClose: true buttonClose: true
buttonCancel: true buttonCancel: true
@ -784,6 +864,24 @@ class Setting extends App.ControllerModal
msg: App.i18n.translateContent(data.message) 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', 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', { 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') 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() @hideAlerts()
@disable(e) @disable(e)
# get params
@params = @formParam(e.target) @params = @formParam(e.target)
# add logo
@params.logo = @logoPreview.attr('src') @params.logo = @logoPreview.attr('src')
@params.locale_default = App.i18n.detectBrowserLocale()
store = (logoResizeDataUrl) => store = (logoResizeDataUrl) =>
@params.logo_resize = logoResizeDataUrl @params.logo_resize = logoResizeDataUrl
@ -354,7 +352,7 @@ class Base extends App.WizardFullScreen
success: (data, status, xhr) => success: (data, status, xhr) =>
if data.result is 'ok' if data.result is 'ok'
for key, value of data.settings for key, value of data.settings
App.Config.set( key, value ) App.Config.set(key, value)
if App.Config.get('system_online_service') if App.Config.get('system_online_service')
@navigate 'getting_started/channel/email_pre_configured' @navigate 'getting_started/channel/email_pre_configured'
else else

View file

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

View file

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

View file

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

View file

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

View file

@ -108,9 +108,7 @@ class Graph extends App.ControllerContent
@render() @render()
render: => update: (data) =>
update = (data) =>
# show only selected lines # show only selected lines
dataNew = {} dataNew = {}
@ -128,13 +126,22 @@ class Graph extends App.ControllerContent
@draw(dataNew) @draw(dataNew)
t = new Date t = new Date
@el.find('#download-chart').html(t.toString()) @el.find('#download-chart').html(t.toString())
new Download( if @downloadWidget
@downloadWidget.update(
config: @config
params: @params
ui: @ui
)
else
@downloadWidget = new Download(
el: @el.find('.js-dataDownload') el: @el.find('.js-dataDownload')
config: @config config: @config
params: @params params: @params
ui: @ui ui: @ui
) )
render: =>
url = "#{@apiPath}/reports/generate" url = "#{@apiPath}/reports/generate"
interval = 5 * 60000 interval = 5 * 60000
if @params.timeRange is 'year' if @params.timeRange is 'year'
@ -142,9 +149,9 @@ class Graph extends App.ControllerContent
if @params.timeRange is 'month' if @params.timeRange is 'month'
interval = 60000 interval = 60000
if @params.timeRange is 'week' if @params.timeRange is 'week'
interval = 40000 interval = 50000
if @params.timeRange is 'day' if @params.timeRange is 'day'
interval = 20000 interval = 30000
if @params.timeRange is 'realtime' if @params.timeRange is 'realtime'
interval = 10000 interval = 10000
@ -164,7 +171,7 @@ class Graph extends App.ControllerContent
) )
processData: true processData: true
success: (data) => success: (data) =>
update(data) @update(data)
@delay(@render, interval, 'report-update', 'page') @delay(@render, interval, 'report-update', 'page')
) )
@ -215,7 +222,7 @@ class Graph extends App.ControllerContent
class Download extends App.Controller class Download extends App.Controller
events: events:
'click .js-dataDownloadBackendSelector': 'tableUpdate' 'click .js-dataDownloadBackendSelector': 'selectBackend'
constructor: (data) -> constructor: (data) ->
@ -225,7 +232,24 @@ class Download extends App.Controller
super super
@render() @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 = [] reports = []
@ -244,44 +268,84 @@ class Download extends App.Controller
@profileSelectedId = key @profileSelectedId = key
profiles.push App.ReportProfile.find(key) profiles.push App.ReportProfile.find(key)
@html App.view('report/download_header')( downloadHeaderHtml = App.view('report/download_header')(
reports: reports reports: reports
profiles: profiles profiles: profiles
downloadBackendSelected: @params.downloadBackendSelected downloadBackendSelected: @params.downloadBackendSelected
metric: @config.metric[@params.metric] metric: @config.metric[@params.metric]
) )
if downloadHeaderHtml isnt @downloadHeaderHtml
@el.find('.js-dataDownloadHeader').html(downloadHeaderHtml)
@downloadHeaderHtml = downloadHeaderHtml
@tableUpdate() @tableUpdate()
tableUpdate: (e) => tableRender: (tickets, count) =>
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()
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) if _.isEmpty(tickets)
@el.find('.js-dataDownloadTable').html('') @$('.js-dataDownloadButton').html('')
else @$('.js-dataDownloadTable').html('')
return
profile_id = 0 profile_id = 0
for key, value of @params.profileSelected for key, value of @params.profileSelected
if value if value
profile_id = key 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}" 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')( @$('.js-dataDownloadButton').html(App.view('report/download_button')(
tickets: tickets
count: count count: count
url: url downloadUrl: downloadUrl
download: downloadUrl ))
)
@el.find('.js-dataDownloadTable').html(html)
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( @ajax(
id: 'report_download' id: 'report_download'
type: 'POST' type: 'POST'
@ -298,15 +362,14 @@ class Download extends App.Controller
downloadBackendSelected: @params.downloadBackendSelected downloadBackendSelected: @params.downloadBackendSelected
) )
processData: true processData: true
success: (data) -> success: (data) =>
App.Collection.loadAssets(data.assets) App.Collection.loadAssets(data.assets)
ticket_collection = [] ticket_collection = []
if data.ticket_ids if data.ticket_ids
for record_id in 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 ticket_collection.push ticket
@tableRender(ticket_collection, data.count)
table(ticket_collection, data.count)
) )
class TimeRangePicker extends App.Controller class TimeRangePicker extends App.Controller

View file

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

View file

@ -6,7 +6,7 @@ class App.TicketCustomer extends App.ControllerModal
content: -> content: ->
configure_attributes = [ configure_attributes = [
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, 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( controller = new App.ControllerForm(
model: model:
@ -18,8 +18,19 @@ class App.TicketCustomer extends App.ControllerModal
onSubmit: (e) => onSubmit: (e) =>
params = @formParam(e.target) 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 = => callback = =>
# close modal # close modal

View file

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

View file

@ -461,6 +461,7 @@ class App.TicketZoom extends App.Controller
ui: @ ui: @
highligher: @highligher highligher: @highligher
ticket_article_ids: @ticket_article_ids ticket_article_ids: @ticket_article_ids
form_id: @form_id
) )
new App.TicketCustomerAvatar( 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 class App.TicketZoomArticleActions extends App.Controller
events: events:
'click [data-type=public]': 'publicInternal' 'click .js-ArticleAction': 'actionPerform'
'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'
constructor: -> constructor: ->
super super
@render() @render()
render: -> render: ->
actions = @actionRow(@article) actions = @actionRow(@ticket, @article)
if actions if actions
@html App.view('ticket_zoom/article_view_actions')( @html App.view('ticket_zoom/article_view_actions')(
@ -25,371 +17,31 @@ class App.TicketZoomArticleActions extends App.Controller
else else
@html '' @html ''
publicInternal: (e) => actionRow: (ticket, article) ->
e.preventDefault() actionConfig = App.Config.get('TicketZoomArticleAction')
articleContainer = $(e.target).closest('.ticket-article-item') keys = _.keys(actionConfig).sort()
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 []
actions = [] actions = []
if article.internal is true for key in keys
actions = [ config = actionConfig[key]
{ if config
name: 'set to public' actions = config.action(actions, ticket, article, @)
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: '#'
}
actions actions
facebookFeedReply: (e) => actionPerform: (e) =>
e.preventDefault() e.preventDefault()
type = App.TicketArticleType.findByAttribute('name', 'facebook feed comment') articleContainer = $(e.target).closest('.ticket-article-item')
@scrollToCompose() type = $(e.currentTarget).attr('data-type')
ticket = App.Ticket.fullLocal(@ticket.id)
article = App.TicketArticle.fullLocal(@article.id)
# empty form actionConfig = App.Config.get('TicketZoomArticleAction')
articleNew = { keys = _.keys(actionConfig).sort()
to: '' actions = []
cc: '' for key in keys
body: '' config = actionConfig[key]
in_reply_to: '' if config
} return if !config.perform(articleContainer, type, ticket, article, @)
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')
)
scrollToCompose: => scrollToCompose: =>
@el.closest('.content').find('.article-add').ScrollTo() @el.closest('.content').find('.article-add').ScrollTo()

View file

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

View file

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

View file

@ -39,9 +39,7 @@ class App.TicketZoomSidebar extends App.ObserverController
tags: @tags tags: @tags
links: @links links: @links
) )
item = @sidebarBackends[key].sidebarItem() @sidebarItems.push @sidebarBackends[key]
if item
@sidebarItems.push item
new App.Sidebar( new App.Sidebar(
el: @el.find('.tabsSidebar') el: @el.find('.tabsSidebar')

View file

@ -1,32 +1,67 @@
class SidebarCustomer extends App.Controller class SidebarCustomer extends App.Controller
sidebarItem: => sidebarItem: =>
return if !@permissionCheck('ticket.agent') return if !@permissionCheck('ticket.agent')
items = { @item = {
head: 'Customer'
name: 'customer' name: 'customer'
icon: 'person' badgeCallback: @badgeRender
actions: [ sidebarHead: 'Customer'
sidebarCallback: @showCustomer
sidebarActions: [
{ {
title: 'Change Customer' title: 'Change Customer'
name: 'customer-change' name: 'customer-change'
callback: @changeCustomer callback: @changeCustomer
}, },
] ]
callback: @showCustomer
} }
return items if @ticket && @ticket.customer_id == 1 return @item if @ticket && @ticket.customer_id == 1
items.actions.push { @item.sidebarActions.push {
title: 'Edit Customer' title: 'Edit Customer'
name: 'customer-edit' name: 'customer-edit'
callback: @editCustomer 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) => showCustomer: (el) =>
@el = el @elSidebar = el
new App.WidgetUser( new App.WidgetUser(
el: @el el: @elSidebar
user_id: @ticket.customer_id user_id: @ticket.customer_id
callback: @badgeRenderLocal
) )
editCustomer: => editCustomer: =>
@ -38,13 +73,13 @@ class SidebarCustomer extends App.Controller
title: 'Users' title: 'Users'
object: 'User' object: 'User'
objects: 'Users' objects: 'Users'
container: @el.closest('.content') container: @elSidebar.closest('.content')
) )
changeCustomer: => changeCustomer: =>
new App.TicketCustomer( new App.TicketCustomer(
ticket_id: @ticket.id ticket_id: @ticket.id
container: @el.closest('.content') container: @elSidebar.closest('.content')
) )
App.Config.set('200-Customer', SidebarCustomer, 'TicketZoomSidebar') App.Config.set('200-Customer', SidebarCustomer, 'TicketZoomSidebar')

View file

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

View file

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

View file

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

View file

@ -145,7 +145,7 @@ class Index extends App.ControllerSubContent
query: @query query: @query
limit: 140 limit: 140
role_ids: role_ids role_ids: role_ids
full: 1 full: true
processData: true, processData: true,
success: (data, status, xhr) => success: (data, status, xhr) =>
App.Collection.loadAssets(data.assets) App.Collection.loadAssets(data.assets)
@ -167,7 +167,7 @@ class Index extends App.ControllerSubContent
data: data:
limit: 50 limit: 50
role_ids: role_ids role_ids: role_ids
full: 1 full: true
processData: true processData: true
success: (data, status, xhr) => success: (data, status, xhr) =>
App.Collection.loadAssets(data.assets) 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 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) => render: (data) =>
if !data if !data
data = @data data = @data
return if !data
user_total = 0 user_total = 0
if data.user.open_ids && data.user.closed_ids if data.user.open_ids && data.user.closed_ids

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -80,6 +80,24 @@ class App.i18n
_instance ?= new _i18nSingleton() _instance ?= new _i18nSingleton()
_instance.mapTime _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 class _i18nSingleton extends Spine.Module
@include App.LogInclude @include App.LogInclude
@ -321,7 +339,8 @@ class _i18nSingleton extends Spine.Module
d = timeObject.getDate() d = timeObject.getDate()
m = timeObject.getMonth() + 1 m = timeObject.getMonth() + 1
y = timeObject.getFullYear() yfull = timeObject.getFullYear()
yshort = timeObject.getYear()-100
S = timeObject.getSeconds() S = timeObject.getSeconds()
M = timeObject.getMinutes() M = timeObject.getMinutes()
H = timeObject.getHours() H = timeObject.getHours()
@ -330,7 +349,8 @@ class _i18nSingleton extends Spine.Module
.replace(/d/, d) .replace(/d/, d)
.replace(/mm/, s(m, 2)) .replace(/mm/, s(m, 2))
.replace(/m/, m) .replace(/m/, m)
.replace(/yyyy/, y) .replace(/yyyy/, yfull)
.replace(/yy/, yshort)
.replace(/SS/, s(S, 2)) .replace(/SS/, s(S, 2))
.replace(/MM/, s(M, 2)) .replace(/MM/, s(M, 2))
.replace(/HH/, s(H, 2)) .replace(/HH/, s(H, 2))

View file

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

View file

@ -5,6 +5,8 @@ class App.Utils
'TD': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'valign', 'width', 'style'] 'TD': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'valign', 'width', 'style']
'TH': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'scope', 'sorted', 'valign', 'width', 'style'] 'TH': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'scope', 'sorted', 'valign', 'width', 'style']
'TR': ['width', 'style'] 'TR': ['width', 'style']
'A': ['href', 'hreflang', 'name', 'rel']
'IMG': ['align', 'alt', 'border', 'height', 'src', 'srcset', 'width', 'style']
@mapCss: @mapCss:
'TABLE': [ 'TABLE': [
@ -14,15 +16,9 @@ class App.Utils
'text-align', 'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width', 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-right-width', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-bottom-width', 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
] ]
'TH': [ 'TH': [
'background', 'background-color', 'color', 'font-size', 'vertical-align', 'background', 'background-color', 'color', 'font-size', 'vertical-align',
@ -31,15 +27,10 @@ class App.Utils
'text-align', 'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width', 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-right-width', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-bottom-width', 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
] ]
'TR': [ 'TR': [
'background', 'background-color', 'color', 'font-size', 'vertical-align', 'background', 'background-color', 'color', 'font-size', 'vertical-align',
@ -48,15 +39,10 @@ class App.Utils
'text-align', 'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width', 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-right-width', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-bottom-width', 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
'border-left-width',
'border-top-color',
'border-right-color',
'border-bottom-color',
'border-left-color',
] ]
'TD': [ 'TD': [
'background', 'background-color', 'color', 'font-size', 'vertical-align', 'background', 'background-color', 'color', 'font-size', 'vertical-align',
@ -65,15 +51,13 @@ class App.Utils
'text-align', 'text-align',
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing', 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
'border-top-width', 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
'border-right-width', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
'border-bottom-width', 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
'border-left-width',
'border-top-color', ]
'border-right-color', 'IMG': [
'border-bottom-color', 'width', 'height',
'border-left-color',
] ]
# textCleand = App.Utils.textCleanup(rawText) # textCleand = App.Utils.textCleanup(rawText)
@ -230,7 +214,7 @@ class App.Utils
# remove comments # remove comments
@_removeComments(html) @_removeComments(html)
# remove work markup # remove word markup
@_removeWordMarkup(html) @_removeWordMarkup(html)
# remove tags, keep content # remove tags, keep content
@ -251,7 +235,7 @@ class App.Utils
# remove comments # remove comments
@_removeComments(html) @_removeComments(html)
# remove work markup # remove word markup
@_removeWordMarkup(html) @_removeWordMarkup(html)
# remove tags, keep content # remove tags, keep content
@ -275,11 +259,11 @@ class App.Utils
# remove comments # remove comments
@_removeComments(html) @_removeComments(html)
# remove work markup # remove word markup
@_removeWordMarkup(html) @_removeWordMarkup(html)
# remove tags, keep content # remove tags, keep content
html.find('a, font, small, time, form, label').replaceWith( -> html.find('font, small, time, form, label').replaceWith( ->
$(@).contents() $(@).contents()
) )
@ -303,7 +287,7 @@ class App.Utils
) )
# remove tags & content # 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 # remove style and class
@_cleanAttributes(html) @_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 = text.replace(/http(s|):\/\/[-A-Za-z0-9+&@#\/%?=~_\|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/img, placeholder)
text.length 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) -> @getRecipientArticle: (ticket, article, article_created_by, type, email_addresses = [], all) ->
# empty form # empty form
@ -954,16 +961,18 @@ class App.Utils
# check if article sender is local # check if article sender is local
senderIsLocal = false senderIsLocal = false
if !_.isEmpty(article.from) if !_.isEmpty(article.from)
senders = emailAddresses.parseAddressList(article.from) senders = App.Utils.parseAddressListLocal(article.from)
if senders && senders[0] && senders[0].address if senders
senderIsLocal = isLocalAddress(senders[0].address) for sender in senders
if sender && sender.match('@')
senderIsLocal = isLocalAddress(sender)
# check if article recipient is local # check if article recipient is local
recipientIsLocal = false recipientIsLocal = false
if !_.isEmpty(article.to) if !_.isEmpty(article.to)
recipients = emailAddresses.parseAddressList(article.to) recipients = App.Utils.parseAddressListLocal(article.to)
if recipients && recipients[0] && recipients[0].address if recipients && recipients[0]
recipientIsLocal = isLocalAddress(recipients[0].address) recipientIsLocal = isLocalAddress(recipients[0])
# sender is local # sender is local
if senderIsLocal if senderIsLocal
@ -987,14 +996,14 @@ class App.Utils
# filter for uniq recipients # filter for uniq recipients
recipientAddresses = {} recipientAddresses = {}
addAddresses = (addressLine, line) -> addAddresses = (addressLine, line) ->
lineNew = '' lineNew = ''
recipients = emailAddresses.parseAddressList(addressLine) recipients = App.Utils.parseAddressListLocal(addressLine)
if !_.isEmpty(recipients) if !_.isEmpty(recipients)
for recipient in recipients for recipient in recipients
if !_.isEmpty(recipient.address) if !_.isEmpty(recipient)
localRecipientAddress = recipient.address.toString().toLowerCase() localRecipientAddress = recipient.toString().toLowerCase()
# check if address is not local # check if address is not local
if !isLocalAddress(localRecipientAddress) if !isLocalAddress(localRecipientAddress)

View file

@ -161,7 +161,14 @@ window.linkify = (function(){
} }
// Push massaged link onto the array // Push massaged link onto the array
// 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 ]); parts.push([ link, href ]);
}
else {
parts.push([ link, undefined ]);
}
}; };
// Push remaining non-link text onto the array. // Push remaining non-link text onto the array.

View file

@ -1,6 +1,6 @@
// email-addresses.js - RFC 5322 email address parser // email-addresses.js - RFC 5322 email address parser
// v 2.0.1 // v 3.0.1
// //
// http://tools.ietf.org/html/rfc5322 // http://tools.ietf.org/html/rfc5322
// //
@ -186,27 +186,7 @@ function parse5322(opts) {
// "First Last" -> "First Last" // "First Last" -> "First Last"
// "First Last" -> "First Last" // "First Last" -> "First Last"
function collapseWhitespace(s) { function collapseWhitespace(s) {
function isWhitespace(c) { return s.replace(/([ \t]|\r\n)+/g, ' ').replace(/^\s*/, '').replace(/\s*$/, '');
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;
} }
// UTF-8 pseudo-production (RFC 6532) // UTF-8 pseudo-production (RFC 6532)
@ -597,10 +577,14 @@ function parse5322(opts) {
return wrap('domain', function domainCheckTLD() { return wrap('domain', function domainCheckTLD() {
var result = or(obsDomain, dotAtom, domainLiteral)(); var result = or(obsDomain, dotAtom, domainLiteral)();
if (opts.rejectTLD) { if (opts.rejectTLD) {
if (result.semantic.indexOf('.') < 0) { if (result && result.semantic && result.semantic.indexOf('.') < 0) {
return null; return null;
} }
} }
// strip all whitespace from domains
if (result) {
result.semantic = result.semantic.replace(/\s+/g, '');
}
return result; 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 // 4.1. Miscellaneous Obsolete Tokens
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control // obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
@ -766,92 +780,186 @@ function parse5322(opts) {
// ast analysis // ast analysis
function findNode(name, root) { function findNode(name, root) {
var i, queue, node; var i, stack, node;
if (root === null || root === undefined) { return null; } if (root === null || root === undefined) { return null; }
queue = [root]; stack = [root];
while (queue.length > 0) { while (stack.length > 0) {
node = queue.shift(); node = stack.pop();
if (node.name === name) { if (node.name === name) {
return node; return node;
} }
for (i = 0; i < node.children.length; i += 1) { for (i = node.children.length - 1; i >= 0; i -= 1) {
queue.push(node.children[i]); stack.push(node.children[i]);
} }
} }
return null; return null;
} }
function findAllNodes(name, root) { function findAllNodes(name, root) {
var i, queue, node, result; var i, stack, node, result;
if (root === null || root === undefined) { return null; } if (root === null || root === undefined) { return null; }
queue = [root]; stack = [root];
result = []; result = [];
while (queue.length > 0) { while (stack.length > 0) {
node = queue.shift(); node = stack.pop();
if (node.name === name) { if (node.name === name) {
result.push(node); result.push(node);
} }
for (i = 0; i < node.children.length; i += 1) { for (i = node.children.length - 1; i >= 0; i -= 1) {
queue.push(node.children[i]); 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; return result;
} }
function giveResult(ast) { function giveResult(ast) {
function grabSemantic(n) { var addresses, groupsAndMailboxes, i, groupOrMailbox, result;
return n !== null ? n.semantic : null;
}
var i, ret, addresses, addr, name, aspec, local, domain;
if (ast === null) { if (ast === null) {
return null; return null;
} }
ret = { ast: ast }; addresses = [];
addresses = findAllNodes('address', ast);
ret.addresses = []; // An address is a 'group' (i.e. a list of mailboxes) or a 'mailbox'.
for (i = 0; i < addresses.length; i += 1) { groupsAndMailboxes = findAllNodesNoChildren(['group', 'mailbox'], ast);
addr = addresses[i]; for (i = 0; i < groupsAndMailboxes.length; i += 1) {
name = findNode('display-name', addr); groupOrMailbox = groupsAndMailboxes[i];
aspec = findNode('addr-spec', addr); if (groupOrMailbox.name === 'group') {
local = findNode('local-part', aspec); addresses.push(giveResultGroup(groupOrMailbox));
domain = findNode('domain', aspec); } else if (groupOrMailbox.name === 'mailbox') {
ret.addresses.push({ addresses.push(giveResultMailbox(groupOrMailbox));
node: addr, }
}
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: { parts: {
name: name, name: name,
address: aspec, address: aspec,
local: local, local: local,
domain: domain domain: domain,
comments: comments
}, },
type: mailbox.name, // 'mailbox'
name: grabSemantic(name), name: grabSemantic(name),
address: grabSemantic(aspec), address: grabSemantic(aspec),
local: grabSemantic(local), local: grabSemantic(local),
domain: grabSemantic(domain) domain: grabSemantic(domain),
}); groupName: grabSemantic(mailbox.groupName),
};
} }
if (opts.simple) { function grabSemantic(n) {
ret = ret.addresses; return n !== null && n !== undefined ? n.semantic : null;
for (i = 0; i < ret.length; i += 1) { }
delete ret[i].node;
function simplifyResult(result) {
var i;
if (result && result.addresses) {
for (i = 0; i < result.addresses.length; i += 1) {
delete result.addresses[i].node;
} }
} }
return ret; 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, {}); opts = handleOpts(opts, {});
if (opts === null) { return null; } if (opts === null) { return null; }
parseString = opts.input; 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) { if (!opts.strict) {
initialize(); initialize();
opts.strict = true; opts.strict = true;
parsed = addressList(parseString); parsed = startProduction(parseString);
if (opts.partial || !inStr()) { if (opts.partial || !inStr()) {
return giveResult(parsed); return giveResult(parsed);
} }
@ -859,46 +967,51 @@ function parse5322(opts) {
} }
initialize(); initialize();
parsed = addressList(parseString); parsed = startProduction(parseString);
if (!opts.partial && inStr()) { return null; } if (!opts.partial && inStr()) { return null; }
return giveResult(parsed); return giveResult(parsed);
} }
function parseOneAddressSimple(opts) { function parseOneAddressSimple(opts) {
var result; return parse5322(handleOpts(opts, {
oneResult: true,
opts = handleOpts(opts, {
rfc6532: true, rfc6532: true,
simple: true simple: true,
}); startAt: 'address-list',
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];
} }
function parseAddressListSimple(opts) { function parseAddressListSimple(opts) {
var result; return parse5322(handleOpts(opts, {
opts = handleOpts(opts, {
rfc6532: true, rfc6532: true,
simple: true simple: true,
}); startAt: 'address-list',
if (opts === null) { return null; } }));
}
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) { function handleOpts(opts, defs) {
@ -926,24 +1039,28 @@ function handleOpts(opts, defs) {
if (!defs) { return null; } if (!defs) { return null; }
defaults = { defaults = {
rfc6532: false, oneResult: false,
partial: false, partial: false,
rejectTLD: false,
rfc6532: false,
simple: false, simple: false,
startAt: 'address-list',
strict: false, strict: false,
rejectTLD: false
}; };
for (o in defaults) { for (o in defaults) {
if (isNullUndef(opts[o])) { if (isNullUndef(opts[o])) {
opts[o] = !isNullUndef(defs[o]) ? defs[o] : defaults[o]; opts[o] = !isNullUndef(defs[o]) ? defs[o] : defaults[o];
} }
opts[o] = !!opts[o];
} }
return opts; return opts;
} }
parse5322.parseOneAddress = parseOneAddressSimple; parse5322.parseOneAddress = parseOneAddressSimple;
parse5322.parseAddressList = parseAddressListSimple; parse5322.parseAddressList = parseAddressListSimple;
parse5322.parseFrom = parseFromSimple;
parse5322.parseSender = parseSenderSimple;
parse5322.parseReplyTo = parseReplyToSimple;
// in Zammad context, go back to non CommonJS // in Zammad context, go back to non CommonJS
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { // if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {

View file

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

View file

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

View file

@ -26,6 +26,7 @@
- allow custom template as options parameter - 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 - 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 - added rerender method to show correct today if task is longer open the 24 hours
- scroll into view
*/ */
(function(factory){ (function(factory){
@ -515,7 +516,9 @@
) )
) )
this.setValue(); 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; return this;
}, },
@ -757,6 +760,16 @@
zIndex: zIndex 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; return this;
}, },

View file

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

View file

@ -1,16 +1,260 @@
class App.Chat extends App.Model 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 @extend Spine.Model.Ajax
@url: @apiPath + '/chats' @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 = [ @configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, { 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: 'active', display: 'Active', tag: 'active', default: true },
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }, { name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }, { name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }, { name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
{ name: 'updated_at', display: 'Updated', tag: 'datetime', 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 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 @extend Spine.Model.Ajax
@url: @apiPath + '/overviews' @url: @apiPath + '/overviews'
@configure_attributes = [ @configure_attributes = [

View file

@ -200,6 +200,26 @@ class App.Ticket extends App.Model
result = true if objectValue.toString().match(contains_regex) result = true if objectValue.toString().match(contains_regex)
else if condition.operator == 'contains not' else if condition.operator == 'contains not'
result = true if !objectValue.toString().match(contains_regex) 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' else if condition.operator == 'is'
result = true if objectValue.toString().trim().toLowerCase() is loopConditionValue.toString().trim().toLowerCase() result = true if objectValue.toString().trim().toLowerCase() is loopConditionValue.toString().trim().toLowerCase()
else if condition.operator == 'is not' else if condition.operator == 'is not'
@ -224,3 +244,19 @@ class App.Ticket extends App.Model
throw "Unknown operator: #{condition.operator}" throw "Unknown operator: #{condition.operator}"
result 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="newTicket">
<div class="box box--newTicket"> <div class="box box--newTicket">
<div class="page-header"> <div class="page-header">
<h1><%- @T( @head ) %></h1> <h1><%- @T(@head) %></h1>
</div> </div>
<div class="page-content"> <div class="page-content">
<ul class="tabs type-tabs"> <ul class="tabs type-tabs">
<li class="tab u-textTruncate" data-type="phone-in"> <li class="tab u-textTruncate" data-type="phone-in">
<%- @Icon('received-calls', 'tab-icon') %> <%- @Icon('received-calls', 'tab-icon') %>
<%- @T('Received Call') %> <%- @T('Received Call') %>
</li> </li>
<li class="tab u-textTruncate" data-type="phone-out"> <li class="tab u-textTruncate" data-type="phone-out">
<%- @Icon('outbound-calls', 'tab-icon') %> <%- @Icon('outbound-calls', 'tab-icon') %>
<%- @T('Outbound Call') %> <%- @T('Outbound Call') %>
</li> </li>
<li class="tab u-textTruncate" data-type="email-out"> <li class="tab u-textTruncate" data-type="email-out">
<%- @Icon('email', 'tab-icon') %> <%- @Icon('email', 'tab-icon') %>
<%- @T('Send Email') %> <%- @T('Send Email') %>
</li> </li>
</ul> </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"> <form role="form" class="ticket-create">
<input type="hidden" name="formSenderType"/> <input type="hidden" name="formSenderType"/>
<input type="hidden" name="form_id" value="<%= @form_id %>"/> <input type="hidden" name="form_id" value="<%= @form_id %>"/>
@ -46,7 +49,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tabsSidebar vertical"></div> <div class="tabsSidebar vertical"></div>
</div> </div>
<!-- <!--

View file

@ -132,7 +132,20 @@
<p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p> <p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p>
<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;
&lt;script&gt;
$(function() {
$('#feedback-form').ZammadForm({
<span class="js-modal-params"></span>
});
});
&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 id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"&gt;&lt;/script&gt;

View file

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

View file

@ -7,9 +7,7 @@
</div> </div>
</div> </div>
<div class="chat-name"> <div class="chat-name">
<%= @name %> <div class="status-badge js-info"> <span class="js-name js-info u-clickable"><%= @name %><span> #<%= @session.id %>
<div class="info-badge"><%- @Icon('info') %></div>
</div>
</div> </div>
<div class="chat-disconnect js-disconnect"> <div class="chat-disconnect js-disconnect">
<div class="btn btn--action btn--small"><%- @T('disconnect') %></div> <div class="btn btn--action btn--small"><%- @T('disconnect') %></div>
@ -24,6 +22,25 @@
</div> </div>
<div class="chat-body-holder js-scrollHolder"> <div class="chat-body-holder js-scrollHolder">
<div class="chat-body js-body"></div> <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>
<div class="chat-controls"> <div class="chat-controls">
<div class="chat-input"> <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">
<div class="attachment-name"><%= @fileName %></div> <div class="attachment-name"><%= @filename %></div>
<div class="attachment-size"><%= @fileSize %></div> <div class="attachment-size"><%= @humanFileSize(@size) %></div>
<div class="attachment-delete js-delete" data-id="<%= @store_id %>"> <div class="attachment-delete js-delete" data-id="<%= @id %>">
<%- @Icon('diagonal-cross') %><%- @T('Delete File') %> <%- @Icon('diagonal-cross') %><%- @T('Delete File') %>
</div> </div>
</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 <select
class="columnSelect-shadow js-shadow" class="columnSelect-shadow js-shadow"
id="<%= @attribute.id %>" id="<%= @attribute.id %>"
@ -11,6 +14,7 @@
<option value="<%= option.value %>" <%= ' selected' if option.selected %>><%= option.name %></option> <option value="<%= option.value %>" <%= ' selected' if option.selected %>><%= option.name %></option>
<% end %> <% end %>
</select> </select>
<% end %>
<div class="columnSelect-column columnSelect-column--selected js-selected" data-name="<%= @attribute.name %>"> <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> <div class="u-placeholder u-unselectable js-placeholder<%= ' is-hidden' if @values.length %>"><%- @T('Nothing selected') %></div>
<% for option in @attribute.options: %> <% for option in @attribute.options: %>

View file

@ -1,7 +1,7 @@
<% for item in @items: %> <% for item in @items: %>
<div class="sidebar bottom-form-shadow flex hide" data-tab="<%= item.name %>"> <div class="sidebar bottom-form-shadow flex hide" data-tab="<%= item.name %>">
<div class="sidebar-header"> <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="sidebar-header-actions js-actions"></div>
<div class="tabsSidebar-close"> <div class="tabsSidebar-close">
<%- @Icon('long-arrow-right') %> <%- @Icon('long-arrow-right') %>
@ -13,8 +13,6 @@
<% end %> <% end %>
<div class="tabsSidebar-tabs" style="<%- if @dir is 'rtl' then 'margin-right' else 'margin-left' %>: -<%- @scrollbarWidth %>px"> <div class="tabsSidebar-tabs" style="<%- if @dir is 'rtl' then 'margin-right' else 'margin-left' %>: -<%- @scrollbarWidth %>px">
<% for item in @items: %> <% for item in @items: %>
<div class="tabsSidebar-tab" data-tab="<%= item.name %>"> <div class="tabsSidebar-tab" data-tab="<%= item.name %>"></div>
<%- @Icon(item.icon) %>
</div>
<% end %> <% end %>
</div> </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> <div class="js-pager"></div>
<table class="table table-hover<%- " #{@class}" if @class %>"> <table class="table table-hover<% if @class: %> <%= @class %><% end %>">
<thead> <thead>
<tr> <tr>
<% if @sortable: %> <% if @sortable: %>
@ -19,14 +19,10 @@
<th style="width: 40px" class="table-radio"></th> <th style="width: 40px" class="table-radio"></th>
<% end %> <% end %>
<% for header, i in @headers: %> <% 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-head<%= ' js-sort' if @tableId %>">
<div class="table-column-title"><%- @T(header.display) %></div> <div class="table-column-title"><%- @T(header.display) %></div>
<div class="table-column-sortIcon"> <div class="table-column-sortIcon"><% if header.sortOrderIcon: %><%- @Icon(header.sortOrderIcon[0], header.sortOrderIcon[1]) %><% end %></div>
<% if header.sortOrderIcon: %>
<%- @Icon(header.sortOrderIcon[0], header.sortOrderIcon[1]) %>
<% end %>
</div>
</div> </div>
<% if @tableId && !header.unresizable && i < @headers.length - 1: %> <% if @tableId && !header.unresizable && i < @headers.length - 1: %>
<div class="table-col-resize js-col-resize"></div> <div class="table-col-resize js-col-resize"></div>

View file

@ -21,7 +21,7 @@
</td> </td>
<% end %> <% end %>
<% for header in @headers: %> <% for header in @headers: %>
<% value = @P(@object, header.name, @attributes) %> <% value = @P(@object, header.name, @attributes, true) %>
<% if @callbacks: %> <% if @callbacks: %>
<% for attribute, callbacksAll of @callbacks: %> <% for attribute, callbacksAll of @callbacks: %>
<% if attribute is header.name: %> <% if attribute is header.name: %>
@ -31,7 +31,7 @@
<% end %> <% end %>
<% end %> <% 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': %> <% if header.name is 'icon': %>
<%- @Icon('task-state', header.class) %> <%- @Icon('task-state', header.class) %>
<% else if header.icon: %> <% else if header.icon: %>

View file

@ -7,9 +7,10 @@
<p> <p>
<%- @T('Download and install the %s Migration Plugin on your %s instance.', 'OTRS', 'OTRS') %>: <%- @T('Download and install the %s Migration Plugin on your %s instance.', 'OTRS', 'OTRS') %>:
</p> </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://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://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://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://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/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>
<div class="wizard-controls horizontal center"> <div class="wizard-controls horizontal center">
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a> <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="alert alert--info hide js-ticket-count-info" role="alert"><%- @T("There are more than 1000 tickets in the Zendesk system. Due to API rate limit restrictions we can't get the exact number of tickets yet and have to fetch them in batches of 1000. This might take some time, better grab a cup of coffee. The total number of tickets gets updated as soon as the currently known number is surpassed.") %></div>
<div class="wizard-body flex vertical justified"> <div class="wizard-body flex vertical justified">
<table class="progressTable"> <table class="progressTable">
<tr class="js-group"> <tr class="js-groups">
<td><span class="js-done">-</span>/<span class="js-total">-</span> <td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Groups') %></span> <td><span><%- @T('Groups') %></span>
<td class="progressTable-progressCell"> <td class="progressTable-progressCell">
@ -73,7 +73,7 @@
<%- @Icon('checkmark') %> <%- @Icon('checkmark') %>
</div> </div>
</tr> </tr>
<tr class="js-organization"> <tr class="js-organizations">
<td><span class="js-done">-</span>/<span class="js-total">-</span> <td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Organizations') %></span> <td><span><%- @T('Organizations') %></span>
<td class="progressTable-progressCell"> <td class="progressTable-progressCell">
@ -82,7 +82,7 @@
<%- @Icon('checkmark') %> <%- @Icon('checkmark') %>
</div> </div>
</tr> </tr>
<tr class="js-user"> <tr class="js-users">
<td><span class="js-done">-</span>/<span class="js-total">-</span> <td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Users') %></span> <td><span><%- @T('Users') %></span>
<td class="progressTable-progressCell"> <td class="progressTable-progressCell">
@ -91,7 +91,7 @@
<%- @Icon('checkmark') %> <%- @Icon('checkmark') %>
</div> </div>
</tr> </tr>
<tr class="js-ticket"> <tr class="js-tickets">
<td><span class="js-done">-</span>/<span class="js-total">-</span> <td><span class="js-done">-</span>/<span class="js-total">-</span>
<td><span><%- @T('Tickets') %></span> <td><span><%- @T('Tickets') %></span>
<td class="progressTable-progressCell"> <td class="progressTable-progressCell">

View file

@ -20,18 +20,18 @@
<% if @job.result && @job.result.error: %> <% if @job.result && @job.result.error: %>
<p><%- @Ttimestamp(@job.started_at) %></p> <p><%- @Ttimestamp(@job.started_at) %></p>
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div> <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> <p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Counting entries. This may take a while.') %></p>
<% else: %> <% else: %>
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p> <p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p>
<div class="flex"> <div class="flex">
<progress max="<%= @job.result.sum %>" value="<%= @countDone %>"></progress> <progress max="<%= @job.result.total %>" value="<%= @job.result.sum %>"></progress>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% if !_.isEmpty(@job.result) && @countDone: %> <% if !_.isEmpty(@job.result) && @job.result.sum: %>
<ul> <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> <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') %> <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> </ul>

View file

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

View file

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

View file

@ -20,18 +20,18 @@
<% if @job.result && @job.result.error: %> <% if @job.result && @job.result.error: %>
<p><%- @Ttimestamp(@job.started_at) %></p> <p><%- @Ttimestamp(@job.started_at) %></p>
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div> <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> <p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Counting entries. This may take a while.') %></p>
<% else: %> <% else: %>
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p> <p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p>
<div class="flex"> <div class="flex">
<progress max="<%= @job.result.sum %>" value="<%= @countDone %>"></progress> <progress max="<%= @job.result.total %>" value="<%= @job.result.sum %>"></progress>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% if !_.isEmpty(@job.result) && @countDone: %> <% if !_.isEmpty(@job.result) && @job.result.sum: %>
<ul> <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> <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') %> <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> </ul>

View file

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

View file

@ -440,7 +440,7 @@
</div> </div>
<div class="textBubble-footer"> <div class="textBubble-footer">
<div class="textBubble-signatur"><span class="js-signature">/je</span></div> <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> </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