Merge branch 'develop' into cast-boolean-object-attribute
This commit is contained in:
commit
49223a51c7
946 changed files with 22736 additions and 10335 deletions
33
.github/ISSUE_TEMPLATE.md
vendored
33
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,6 +1,18 @@
|
|||
<!--
|
||||
Hi there - thanks for filling an issue. Please ensure the following things before creating an issue - thank you! 🤓
|
||||
Hi there - thanks for filing an issue. Please ensure the following things before creating an issue - thank you! 🤓
|
||||
|
||||
Since november 15th we handle all requests, except real bugs, at our community board.
|
||||
Full explanation: https://community.zammad.org/t/major-change-regarding-github-issues-community-board/21
|
||||
|
||||
Please post:
|
||||
- Feature requests
|
||||
- Development questions
|
||||
- Technical questions
|
||||
|
||||
on the board -> https://community.zammad.org !
|
||||
|
||||
|
||||
If you think you hit a bug, please continue:
|
||||
- Search existing issues and the CHANGELOG.md for your issue - there might be a solution already
|
||||
- Make sure to use the latest version of Zammad if possible
|
||||
- Add the `log/production.log` file from your system. Attention: Make sure no confidential data is in it!
|
||||
|
@ -8,30 +20,33 @@ Hi there - thanks for filling an issue. Please ensure the following things befor
|
|||
- Don't remove the template - otherwise we will close the issue without further comments
|
||||
- Ask questions about Zammad configuration and usage at our mailinglist. See: https://zammad.org/participate
|
||||
|
||||
Note: We always do our best. Unfortunately, sometimes the requests are too much and we can't handle everything at once. If you want to prioritize/escalate your issue, you can do so by means of a support contract (see https://zammad.com/pricing#selfhosted).
|
||||
Note: We always do our best. Unfortunately, sometimes there are too many requests and we can't handle everything at once. If you want to prioritize/escalate your issue, you can do so by means of a support contract (see https://zammad.com/pricing#selfhosted).
|
||||
|
||||
* The upper textblock will be removed automatically when you submit your issue *
|
||||
-->
|
||||
|
||||
### Infos:
|
||||
|
||||
* Used Zammad version:
|
||||
* Used Zammad installation source: (source, package, ...)
|
||||
* Operating system:
|
||||
* Browser + version:
|
||||
* Used Zammad version:
|
||||
* Installation method (source, package, ..):
|
||||
* Operating system:
|
||||
* Database + version:
|
||||
* Elasticsearch version:
|
||||
* Browser + version:
|
||||
|
||||
|
||||
### Expected behavior:
|
||||
|
||||
*
|
||||
*
|
||||
|
||||
|
||||
### Actual behavior:
|
||||
|
||||
*
|
||||
*
|
||||
|
||||
|
||||
### Steps to reproduce the behavior:
|
||||
|
||||
*
|
||||
*
|
||||
|
||||
Yes I'm sure this is a bug and no feature request or a general question.
|
||||
|
|
|
@ -310,7 +310,8 @@ test:integration:es_mysql:
|
|||
- ruby -I test/ test/controllers/search_controller_test.rb
|
||||
- ruby -I test/ test/integration/report_test.rb
|
||||
- ruby -I test/ test/controllers/form_controller_test.rb
|
||||
- ruby -I test/ test/controllers/user_organization_controller_test.rb
|
||||
- ruby -I test/ test/controllers/user_controller_test.rb
|
||||
- ruby -I test/ test/controllers/organization_controller_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:es_postgresql:
|
||||
|
@ -328,7 +329,8 @@ test:integration:es_postgresql:
|
|||
- ruby -I test/ test/controllers/search_controller_test.rb
|
||||
- ruby -I test/ test/integration/report_test.rb
|
||||
- ruby -I test/ test/controllers/form_controller_test.rb
|
||||
- ruby -I test/ test/controllers/user_organization_controller_test.rb
|
||||
- ruby -I test/ test/controllers/user_controller_test.rb
|
||||
- ruby -I test/ test/controllers/organization_controller_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:zendesk_mysql:
|
||||
|
@ -355,24 +357,36 @@ test:integration:zendesk_postgresql:
|
|||
- ruby -I test/ test/integration/zendesk_import_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:otrs_5_mysql:
|
||||
test:integration:otrs_6_mysql:
|
||||
stage: test
|
||||
tags:
|
||||
- core
|
||||
- mysql
|
||||
script:
|
||||
- export RAILS_ENV=test
|
||||
- export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"
|
||||
- export IMPORT_OTRS_ENDPOINT="http://vz1185.test.znuny.com/otrs/public.pl?Action=ZammadMigrator"
|
||||
- rake db:create
|
||||
- rake db:migrate
|
||||
- ruby -I test/ test/integration/otrs_import_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:otrs_5_postgresql:
|
||||
test:integration:otrs_6_postgresql:
|
||||
stage: test
|
||||
tags:
|
||||
- core
|
||||
- postgresql
|
||||
script:
|
||||
- export RAILS_ENV=test
|
||||
- export IMPORT_OTRS_ENDPOINT="http://vz1185.test.znuny.com/otrs/public.pl?Action=ZammadMigrator"
|
||||
- rake db:create
|
||||
- rake db:migrate
|
||||
- ruby -I test/ test/integration/otrs_import_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:otrs_5:
|
||||
stage: test
|
||||
tags:
|
||||
- core
|
||||
script:
|
||||
- export RAILS_ENV=test
|
||||
- export IMPORT_OTRS_ENDPOINT="http://vz1109.demo.znuny.com/otrs/public.pl?Action=ZammadMigrator"
|
||||
|
|
61
.rubocop.yml
61
.rubocop.yml
|
@ -45,29 +45,29 @@ Style/TrailingCommaInArguments:
|
|||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-array-commas'
|
||||
Enabled: false
|
||||
|
||||
Style/SpaceInsideParens:
|
||||
Layout/SpaceInsideParens:
|
||||
Description: 'No spaces after ( or before ).'
|
||||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
|
||||
Enabled: false
|
||||
|
||||
Style/SpaceAfterMethodName:
|
||||
Layout/SpaceAfterMethodName:
|
||||
Description: >-
|
||||
Do not put a space between a method name and the opening
|
||||
parenthesis in a method definition.
|
||||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parens-no-spaces'
|
||||
Enabled: false
|
||||
|
||||
Style/LeadingCommentSpace:
|
||||
Layout/LeadingCommentSpace:
|
||||
Description: 'Comments should start with a space.'
|
||||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#hash-space'
|
||||
Enabled: false
|
||||
|
||||
Style/MethodCallParentheses:
|
||||
Style/MethodCallWithoutArgsParentheses:
|
||||
Description: 'Do not use parentheses for method calls with no arguments.'
|
||||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-args-no-parens'
|
||||
Enabled: false
|
||||
|
||||
Style/SpaceInsideBrackets:
|
||||
Layout/SpaceInsideBrackets:
|
||||
Description: 'No spaces after [ or before ].'
|
||||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-spaces-braces'
|
||||
Enabled: false
|
||||
|
@ -83,19 +83,19 @@ Style/MethodDefParentheses:
|
|||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#method-parens'
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyLinesAroundClassBody:
|
||||
Layout/EmptyLinesAroundClassBody:
|
||||
Description: "Keeps track of empty lines around class bodies."
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyLinesAroundMethodBody:
|
||||
Layout/EmptyLinesAroundMethodBody:
|
||||
Description: "Keeps track of empty lines around method bodies."
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyLinesAroundBlockBody:
|
||||
Layout/EmptyLinesAroundBlockBody:
|
||||
Description: "Keeps track of empty lines around block bodies."
|
||||
Enabled: false
|
||||
|
||||
Style/EmptyLinesAroundModuleBody:
|
||||
Layout/EmptyLinesAroundModuleBody:
|
||||
Description: "Keeps track of empty lines around module bodies."
|
||||
Enabled: false
|
||||
|
||||
|
@ -143,17 +143,29 @@ Rails/HasAndBelongsToMany:
|
|||
# StyleGuide: 'https://github.com/bbatsov/rails-style-guide#has-many-through'
|
||||
Enabled: false
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Description: >-
|
||||
Use methods that skips model validations with caution.
|
||||
See reference for more information.
|
||||
Reference: 'http://guides.rubyonrails.org/active_record_validations.html#skipping-validations'
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- test/**/*
|
||||
|
||||
Style/ClassAndModuleChildren:
|
||||
Description: 'Checks style of children classes and modules.'
|
||||
Enabled: false
|
||||
|
||||
Style/FileName:
|
||||
Naming/FileName:
|
||||
Description: 'Use snake_case for source file names.'
|
||||
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- 'script/websocket-server.rb'
|
||||
|
||||
Naming/VariableNumber:
|
||||
Description: 'Use the configured style when numbering variables.'
|
||||
Enabled: false
|
||||
|
||||
# 2.0
|
||||
|
||||
|
@ -184,8 +196,23 @@ Metrics/ModuleLength:
|
|||
Description: 'Avoid modules longer than 100 lines of code.'
|
||||
Enabled: false
|
||||
|
||||
Metrics/BlockLength:
|
||||
Enabled: false
|
||||
|
||||
Lint/RescueWithoutErrorClass:
|
||||
Enabled: false
|
||||
|
||||
Rails/ApplicationRecord:
|
||||
Enabled: false
|
||||
|
||||
# TODO
|
||||
|
||||
Rails/HasManyOrHasOneDependent:
|
||||
Enabled: false
|
||||
|
||||
Style/DateTime:
|
||||
Enabled: false
|
||||
|
||||
Style/Documentation:
|
||||
Description: 'Document classes and non-namespace modules.'
|
||||
Enabled: false
|
||||
|
@ -193,7 +220,7 @@ Style/Documentation:
|
|||
Lint/UselessAssignment:
|
||||
Enabled: false
|
||||
|
||||
Style/ExtraSpacing:
|
||||
Layout/ExtraSpacing:
|
||||
Description: 'Do not use unnecessary spacing.'
|
||||
Enabled: false
|
||||
|
||||
|
@ -215,4 +242,14 @@ Style/NumericPredicate:
|
|||
AutoCorrect: false
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "**/*_spec.rb"
|
||||
- "**/*_spec.rb"
|
||||
|
||||
Lint/AmbiguousBlockAssociation:
|
||||
Description: >-
|
||||
Checks for ambiguous block association with method when param passed without
|
||||
parentheses.
|
||||
StyleGuide: '#syntax'
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- "**/*_spec.rb"
|
||||
- "**/*_examples.rb"
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.4.1
|
||||
2.4.2
|
||||
|
|
|
@ -19,7 +19,7 @@ services:
|
|||
- mysql
|
||||
language: ruby
|
||||
rvm:
|
||||
- 2.4.1
|
||||
- 2.4.2
|
||||
before_install:
|
||||
- git fetch --unshallow
|
||||
- sudo apt-get -qq update
|
||||
|
@ -62,3 +62,4 @@ script:
|
|||
after_success:
|
||||
- if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-build.sh; fi
|
||||
- if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-compose-build.sh; fi
|
||||
- if [ "${DB}" = "mysql" ]; then contrib/travis-ci.org/trigger-docker-univention-build.sh; fi
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Change Log
|
||||
|
||||
## [2.2.0](https://github.com/zammad/zammad/tree/2.2.0) (2017-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/2.1.0...2.2.0)
|
||||
## [2.4.0](https://github.com/zammad/zammad/tree/2.4.0) (2018-xx-xx)
|
||||
[Full Changelog](https://github.com/zammad/zammad/compare/2.3.0...2.4.0)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
|
|
133
Gemfile
133
Gemfile
|
@ -1,111 +1,131 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
ruby '2.4.1'
|
||||
|
||||
# core - base
|
||||
ruby '2.4.2'
|
||||
gem 'rails', '5.1.4'
|
||||
gem 'rails-observers'
|
||||
|
||||
# core - rails additions
|
||||
gem 'activerecord-session_store'
|
||||
|
||||
# Bundle edge Rails instead:
|
||||
#gem 'rails', :git => 'git://github.com/rails/rails.git'
|
||||
|
||||
gem 'composite_primary_keys'
|
||||
gem 'json'
|
||||
gem 'rails-observers'
|
||||
|
||||
# Supported DBs
|
||||
# core - application servers
|
||||
gem 'puma', group: :puma
|
||||
gem 'unicorn', group: :unicorn
|
||||
|
||||
# core - supported ORMs
|
||||
gem 'activerecord-nulldb-adapter', group: :nulldb
|
||||
gem 'mysql2', group: :mysql
|
||||
gem 'pg', group: :postgres
|
||||
|
||||
# core - asynchrous task execution
|
||||
gem 'daemons'
|
||||
gem 'delayed_job_active_record'
|
||||
|
||||
# core - websocket
|
||||
gem 'em-websocket'
|
||||
gem 'eventmachine'
|
||||
|
||||
# core - password security
|
||||
gem 'argon2'
|
||||
|
||||
# performance - Memcached
|
||||
gem 'dalli'
|
||||
|
||||
# asset handling
|
||||
group :assets do
|
||||
gem 'sass-rails' #, github: 'rails/sass-rails'
|
||||
# asset handling - coffee-script
|
||||
gem 'coffee-rails'
|
||||
gem 'coffee-script-source'
|
||||
|
||||
gem 'sprockets'
|
||||
|
||||
gem 'uglifier'
|
||||
# asset handling - frontend templating
|
||||
gem 'eco'
|
||||
|
||||
# asset handling - SASS
|
||||
gem 'sass-rails'
|
||||
|
||||
# asset handling - pipeline
|
||||
gem 'sprockets'
|
||||
gem 'uglifier'
|
||||
end
|
||||
|
||||
gem 'autoprefixer-rails'
|
||||
|
||||
# asset handling - javascript execution for e.g. linux
|
||||
gem 'execjs'
|
||||
gem 'libv8'
|
||||
gem 'therubyracer'
|
||||
|
||||
# authentication - provider
|
||||
gem 'doorkeeper'
|
||||
gem 'oauth2'
|
||||
|
||||
# authentication - third party
|
||||
gem 'omniauth'
|
||||
gem 'omniauth-oauth2'
|
||||
gem 'omniauth-facebook'
|
||||
gem 'omniauth-github'
|
||||
gem 'omniauth-gitlab'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-linkedin-oauth2'
|
||||
gem 'omniauth-twitter'
|
||||
gem 'omniauth-microsoft-office365'
|
||||
gem 'omniauth-oauth2'
|
||||
gem 'omniauth-twitter'
|
||||
gem 'omniauth-weibo-oauth2'
|
||||
|
||||
gem 'twitter'
|
||||
gem 'telegramAPI'
|
||||
# channels
|
||||
gem 'koala'
|
||||
gem 'mail'
|
||||
gem 'valid_email2'
|
||||
gem 'telegramAPI'
|
||||
gem 'twitter'
|
||||
|
||||
# channels - email additions
|
||||
gem 'htmlentities'
|
||||
|
||||
gem 'mail', '2.6.6'
|
||||
gem 'mime-types'
|
||||
gem 'valid_email2'
|
||||
|
||||
# feature - business hours
|
||||
gem 'biz'
|
||||
|
||||
gem 'composite_primary_keys'
|
||||
gem 'delayed_job_active_record'
|
||||
gem 'daemons'
|
||||
|
||||
gem 'simple-rss'
|
||||
|
||||
# e. g. on linux we need a javascript execution
|
||||
gem 'libv8'
|
||||
gem 'execjs'
|
||||
gem 'therubyracer'
|
||||
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
|
||||
gem 'net-ldap'
|
||||
|
||||
# password security
|
||||
gem 'argon2'
|
||||
# feature - signature diffing
|
||||
gem 'diffy'
|
||||
|
||||
# feature - excel output
|
||||
gem 'writeexcel'
|
||||
gem 'icalendar'
|
||||
gem 'icalendar-recurrence'
|
||||
|
||||
# feature - device logging
|
||||
gem 'browser'
|
||||
|
||||
# feature - iCal export
|
||||
gem 'icalendar'
|
||||
gem 'icalendar-recurrence'
|
||||
|
||||
# integrations
|
||||
gem 'slack-notifier'
|
||||
gem 'clearbit'
|
||||
gem 'net-ldap'
|
||||
gem 'slack-notifier'
|
||||
gem 'zendesk_api'
|
||||
gem 'viewpoint'
|
||||
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
|
||||
|
||||
# integrations - exchange
|
||||
gem 'autodiscover', git: 'https://github.com/thorsteneckel/autodiscover.git'
|
||||
|
||||
# event machine
|
||||
gem 'eventmachine'
|
||||
gem 'em-websocket'
|
||||
|
||||
gem 'diffy'
|
||||
gem 'rubyntlm', git: 'https://github.com/wimm/rubyntlm.git'
|
||||
gem 'viewpoint'
|
||||
|
||||
# Gems used only for develop/test and not required
|
||||
# in production environments by default.
|
||||
group :development, :test do
|
||||
|
||||
# test frameworks
|
||||
gem 'rspec-rails'
|
||||
gem 'test-unit'
|
||||
gem 'spring'
|
||||
gem 'spring-commands-rspec'
|
||||
|
||||
# test DB
|
||||
gem 'sqlite3'
|
||||
|
||||
# code coverage
|
||||
gem 'coveralls', require: false
|
||||
gem 'simplecov'
|
||||
gem 'simplecov-rcov'
|
||||
gem 'coveralls', require: false
|
||||
|
||||
# UI tests w/ Selenium
|
||||
gem 'selenium-webdriver', '2.53.4'
|
||||
|
@ -120,9 +140,9 @@ group :development, :test do
|
|||
gem 'guard-symlink', require: false
|
||||
|
||||
# code QA
|
||||
gem 'coffeelint'
|
||||
gem 'pre-commit'
|
||||
gem 'rubocop'
|
||||
gem 'coffeelint'
|
||||
|
||||
# changelog generation
|
||||
gem 'github_changelog_generator'
|
||||
|
@ -130,17 +150,14 @@ group :development, :test do
|
|||
# Setting ENV for testing purposes
|
||||
gem 'figaro'
|
||||
|
||||
# Use Factory Girl for generating random test data
|
||||
gem 'factory_girl_rails'
|
||||
# Use Factory Bot for generating random test data
|
||||
gem 'factory_bot_rails'
|
||||
|
||||
# mock http calls
|
||||
gem 'webmock'
|
||||
end
|
||||
|
||||
gem 'puma', group: :puma
|
||||
gem 'unicorn', group: :unicorn
|
||||
|
||||
# load onw gem's
|
||||
# load onw gems for development and testing purposes
|
||||
local_gemfile = File.join(File.dirname(__FILE__), 'Gemfile.local')
|
||||
if File.exist?(local_gemfile)
|
||||
eval_gemfile local_gemfile
|
||||
|
|
182
Gemfile.lock
182
Gemfile.lock
|
@ -65,22 +65,22 @@ GEM
|
|||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
arel (8.0.0)
|
||||
argon2 (1.1.3)
|
||||
argon2 (1.1.4)
|
||||
ffi (~> 1.9)
|
||||
ffi-compiler (~> 0.1)
|
||||
ast (2.3.0)
|
||||
autoprefixer-rails (7.1.3)
|
||||
autoprefixer-rails (7.1.6)
|
||||
execjs
|
||||
biz (1.7.0)
|
||||
clavius (~> 1.0)
|
||||
tzinfo
|
||||
browser (2.5.1)
|
||||
browser (2.5.2)
|
||||
buftok (0.2.0)
|
||||
builder (3.2.3)
|
||||
childprocess (0.7.1)
|
||||
childprocess (0.8.0)
|
||||
ffi (~> 1.0, >= 1.0.11)
|
||||
clavius (1.0.3)
|
||||
clearbit (0.2.7)
|
||||
clearbit (0.2.8)
|
||||
nestful (~> 1.1.0)
|
||||
coderay (1.1.2)
|
||||
coffee-rails (4.2.2)
|
||||
|
@ -90,22 +90,24 @@ GEM
|
|||
coffee-script-source
|
||||
execjs
|
||||
coffee-script-source (1.12.2)
|
||||
coffeelint (1.16.0)
|
||||
coffeelint (1.16.1)
|
||||
coffee-script
|
||||
execjs
|
||||
json
|
||||
composite_primary_keys (10.0.0)
|
||||
composite_primary_keys (10.0.1)
|
||||
activerecord (~> 5.1.0)
|
||||
concurrent-ruby (1.0.5)
|
||||
coveralls (0.8.21)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov (~> 0.14.1)
|
||||
term-ansicolor (~> 1.3)
|
||||
thor (~> 0.19.4)
|
||||
tins (~> 1.6)
|
||||
coveralls (0.7.1)
|
||||
multi_json (~> 1.3)
|
||||
rest-client
|
||||
simplecov (>= 0.7)
|
||||
term-ansicolor
|
||||
thor
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
daemons (1.2.4)
|
||||
crass (1.0.3)
|
||||
daemons (1.2.5)
|
||||
dalli (2.7.6)
|
||||
delayed_job (4.1.3)
|
||||
activesupport (>= 3.0, < 5.2)
|
||||
delayed_job_active_record (4.1.2)
|
||||
|
@ -127,15 +129,15 @@ GEM
|
|||
eventmachine (>= 0.12.9)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
equalizer (0.0.11)
|
||||
erubi (1.6.1)
|
||||
erubi (1.7.0)
|
||||
eventmachine (1.2.5)
|
||||
execjs (2.7.0)
|
||||
factory_girl (4.8.0)
|
||||
factory_bot (4.8.2)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_girl_rails (4.8.0)
|
||||
factory_girl (~> 4.8.0)
|
||||
factory_bot_rails (4.8.2)
|
||||
factory_bot (~> 4.8.2)
|
||||
railties (>= 3.0.0)
|
||||
faraday (0.11.0)
|
||||
faraday (0.12.2)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
faraday-http-cache (2.0.0)
|
||||
faraday (~> 0.8)
|
||||
|
@ -154,7 +156,7 @@ GEM
|
|||
rainbow (>= 2.1)
|
||||
rake (>= 10.0)
|
||||
retriable (~> 2.1)
|
||||
globalid (0.4.0)
|
||||
globalid (0.4.1)
|
||||
activesupport (>= 4.2.0)
|
||||
guard (2.14.1)
|
||||
formatador (>= 0.2.4)
|
||||
|
@ -174,20 +176,21 @@ GEM
|
|||
guard-symlink (0.1.1)
|
||||
guard
|
||||
guard-compat (~> 1.1)
|
||||
hashdiff (0.3.6)
|
||||
hashdiff (0.3.7)
|
||||
hashie (3.5.6)
|
||||
htmlentities (4.3.4)
|
||||
http (2.2.2)
|
||||
http (3.0.0)
|
||||
addressable (~> 2.3)
|
||||
http-cookie (~> 1.0)
|
||||
http-form_data (~> 1.0.1)
|
||||
http-form_data (>= 2.0.0.pre.pre2, < 3)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
http-form_data (1.0.3)
|
||||
http-form_data (2.0.0)
|
||||
http_parser.rb (0.6.0)
|
||||
httpclient (2.8.3)
|
||||
i18n (0.8.6)
|
||||
i18n (0.9.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.4.1)
|
||||
icalendar-recurrence (1.1.2)
|
||||
icalendar (~> 2.0)
|
||||
|
@ -210,25 +213,26 @@ GEM
|
|||
logging (2.2.2)
|
||||
little-plugger (~> 1.1)
|
||||
multi_json (~> 1.10)
|
||||
loofah (2.0.3)
|
||||
loofah (2.1.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lumberjack (1.0.12)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memoizable (0.4.2)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
method_source (0.8.2)
|
||||
method_source (0.9.0)
|
||||
mime-types (2.99.3)
|
||||
mini_portile2 (2.3.0)
|
||||
minitest (5.10.3)
|
||||
multi_json (1.12.2)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mysql2 (0.4.9)
|
||||
mysql2 (0.4.10)
|
||||
naught (1.1.0)
|
||||
nenv (0.3.0)
|
||||
nestful (1.1.1)
|
||||
net-ldap (0.16.0)
|
||||
nestful (1.1.3)
|
||||
net-ldap (0.16.1)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.1.0)
|
||||
nokogiri (1.8.1)
|
||||
|
@ -246,7 +250,7 @@ GEM
|
|||
rack (>= 1.2, < 3)
|
||||
octokit (4.7.0)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
omniauth (1.6.1)
|
||||
omniauth (1.7.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-facebook (4.0.0)
|
||||
|
@ -280,24 +284,24 @@ GEM
|
|||
omniauth-weibo-oauth2 (0.4.5)
|
||||
omniauth (~> 1.5)
|
||||
omniauth-oauth2 (>= 1.4.0)
|
||||
parser (2.4.0.0)
|
||||
ast (~> 2.2)
|
||||
parallel (1.12.0)
|
||||
parser (2.4.0.2)
|
||||
ast (~> 2.3)
|
||||
pg (0.21.0)
|
||||
pluginator (1.5.0)
|
||||
power_assert (1.1.0)
|
||||
power_assert (1.1.1)
|
||||
powerpack (0.1.1)
|
||||
pre-commit (0.35.0)
|
||||
pre-commit (0.37.0)
|
||||
pluginator (~> 1.5)
|
||||
pry (0.10.4)
|
||||
pry (0.11.3)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
public_suffix (3.0.0)
|
||||
puma (3.10.0)
|
||||
method_source (~> 0.9.0)
|
||||
public_suffix (3.0.1)
|
||||
puma (3.11.0)
|
||||
rack (2.0.3)
|
||||
rack-livereload (0.3.16)
|
||||
rack
|
||||
rack-test (0.7.0)
|
||||
rack-test (0.8.2)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (5.1.4)
|
||||
actioncable (= 5.1.4)
|
||||
|
@ -327,7 +331,7 @@ GEM
|
|||
rainbow (2.2.2)
|
||||
rake
|
||||
raindrops (0.19.0)
|
||||
rake (12.1.0)
|
||||
rake (12.3.0)
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
|
@ -337,39 +341,40 @@ GEM
|
|||
mime-types (>= 1.16, < 3.0)
|
||||
netrc (~> 0.7)
|
||||
retriable (2.1.0)
|
||||
rspec-core (3.6.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-expectations (3.6.0)
|
||||
rspec-core (3.7.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-expectations (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-mocks (3.6.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-mocks (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-rails (3.6.1)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-rails (3.7.2)
|
||||
actionpack (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-core (~> 3.6.0)
|
||||
rspec-expectations (~> 3.6.0)
|
||||
rspec-mocks (~> 3.6.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-support (3.6.0)
|
||||
rubocop (0.42.0)
|
||||
parser (>= 2.3.1.1, < 3.0)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
rspec-mocks (~> 3.7.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-support (3.7.0)
|
||||
rubocop (0.51.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.3.3.1, < 3.0)
|
||||
powerpack (~> 0.1)
|
||||
rainbow (>= 1.99.1, < 3.0)
|
||||
rainbow (>= 2.2.2, < 3.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (~> 1.0, >= 1.0.1)
|
||||
ruby-progressbar (1.8.1)
|
||||
ruby-progressbar (1.9.0)
|
||||
ruby_dep (1.5.0)
|
||||
rubyzip (1.2.1)
|
||||
safe_yaml (1.0.4)
|
||||
sass (3.5.1)
|
||||
sass (3.5.3)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
sass-rails (5.0.6)
|
||||
sass-rails (5.0.7)
|
||||
railties (>= 4.0.0, < 6)
|
||||
sass (~> 3.1)
|
||||
sprockets (>= 2.8, < 4.0)
|
||||
|
@ -383,9 +388,8 @@ GEM
|
|||
rubyzip (~> 1.0)
|
||||
websocket (~> 1.0)
|
||||
shellany (0.0.1)
|
||||
simple-rss (1.3.1)
|
||||
simple_oauth (0.3.1)
|
||||
simplecov (0.14.1)
|
||||
simplecov (0.15.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
|
@ -393,11 +397,6 @@ GEM
|
|||
simplecov-rcov (0.2.3)
|
||||
simplecov (>= 0.4.1)
|
||||
slack-notifier (2.3.1)
|
||||
slop (3.6.0)
|
||||
spring (2.0.2)
|
||||
activesupport (>= 4.2)
|
||||
spring-commands-rspec (1.0.4)
|
||||
spring (>= 0.9.1)
|
||||
sprockets (3.7.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
|
@ -410,26 +409,27 @@ GEM
|
|||
rest-client (~> 1.7, >= 1.7.3)
|
||||
term-ansicolor (1.6.0)
|
||||
tins (~> 1.0)
|
||||
test-unit (3.2.5)
|
||||
test-unit (3.2.6)
|
||||
power_assert
|
||||
therubyracer (0.12.3)
|
||||
libv8 (~> 3.16.14.15)
|
||||
ref
|
||||
thor (0.19.4)
|
||||
thor (0.20.0)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.8)
|
||||
tins (1.15.0)
|
||||
twitter (6.1.0)
|
||||
addressable (~> 2.5)
|
||||
tins (1.15.1)
|
||||
twitter (6.2.0)
|
||||
addressable (~> 2.3)
|
||||
buftok (~> 0.2.0)
|
||||
equalizer (= 0.0.11)
|
||||
faraday (~> 0.11.0)
|
||||
http (~> 2.1)
|
||||
equalizer (~> 0.0.11)
|
||||
http (~> 3.0)
|
||||
http-form_data (~> 2.0)
|
||||
http_parser.rb (~> 0.6.0)
|
||||
memoizable (~> 0.4.2)
|
||||
naught (~> 1.1)
|
||||
simple_oauth (~> 0.3.1)
|
||||
tzinfo (1.2.3)
|
||||
memoizable (~> 0.4.0)
|
||||
multipart-post (~> 2.0)
|
||||
naught (~> 1.0)
|
||||
simple_oauth (~> 0.3.0)
|
||||
tzinfo (1.2.4)
|
||||
thread_safe (~> 0.1)
|
||||
uglifier (3.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
|
@ -437,10 +437,10 @@ GEM
|
|||
unf_ext
|
||||
unf_ext (0.0.7.4)
|
||||
unicode-display_width (1.3.0)
|
||||
unicorn (5.3.0)
|
||||
unicorn (5.3.1)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
valid_email2 (2.0.1)
|
||||
valid_email2 (2.1.0)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
viewpoint (1.1.0)
|
||||
|
@ -448,16 +448,16 @@ GEM
|
|||
logging
|
||||
nokogiri
|
||||
rubyntlm
|
||||
webmock (3.0.1)
|
||||
webmock (3.1.1)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
websocket (1.2.4)
|
||||
websocket (1.2.5)
|
||||
websocket-driver (0.6.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
websocket-extensions (0.1.3)
|
||||
writeexcel (1.0.5)
|
||||
zendesk_api (1.14.4)
|
||||
zendesk_api (1.16.0)
|
||||
faraday (~> 0.9)
|
||||
hashie (>= 3.5.2, < 4.0.0)
|
||||
inflection
|
||||
|
@ -482,6 +482,7 @@ DEPENDENCIES
|
|||
composite_primary_keys
|
||||
coveralls
|
||||
daemons
|
||||
dalli
|
||||
delayed_job_active_record
|
||||
diffy
|
||||
doorkeeper
|
||||
|
@ -489,7 +490,7 @@ DEPENDENCIES
|
|||
em-websocket
|
||||
eventmachine
|
||||
execjs
|
||||
factory_girl_rails
|
||||
factory_bot_rails
|
||||
figaro
|
||||
github_changelog_generator
|
||||
guard
|
||||
|
@ -501,7 +502,7 @@ DEPENDENCIES
|
|||
json
|
||||
koala
|
||||
libv8
|
||||
mail
|
||||
mail (= 2.6.6)
|
||||
mime-types
|
||||
mysql2
|
||||
net-ldap
|
||||
|
@ -528,12 +529,9 @@ DEPENDENCIES
|
|||
rubyntlm!
|
||||
sass-rails
|
||||
selenium-webdriver (= 2.53.4)
|
||||
simple-rss
|
||||
simplecov
|
||||
simplecov-rcov
|
||||
slack-notifier
|
||||
spring
|
||||
spring-commands-rspec
|
||||
sprockets
|
||||
sqlite3
|
||||
telegramAPI
|
||||
|
@ -549,7 +547,7 @@ DEPENDENCIES
|
|||
zendesk_api
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.4.1p111
|
||||
ruby 2.4.2p198
|
||||
|
||||
BUNDLED WITH
|
||||
1.15.4
|
||||
1.16.0
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
Zammad is a web based open source helpdesk/customer support system with many
|
||||
features to manage customer communication via several channels like telephone,
|
||||
facebook, twitter, chat and e-mails. It is distributed under the GNU AFFERO
|
||||
General Public License (AGPL) and tested on Linux, Solaris, AIX, FreeBSD,
|
||||
OpenBSD and Mac OS 10.x. Do you receive many e-mails and want to answer them
|
||||
with a team of agents?
|
||||
General Public License (AGPL).
|
||||
|
||||
Do you receive many e-mails and want to answer them with a team of agents?
|
||||
|
||||
You're going to love Zammad!
|
||||
|
||||
|
|
0
Rakefile
Normal file → Executable file
0
Rakefile
Normal file → Executable file
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
2.2.x
|
||||
2.4.x
|
||||
|
|
|
@ -700,6 +700,8 @@ class App.ControllerModal extends App.Controller
|
|||
headPrefix: ''
|
||||
shown: true
|
||||
closeOnAnyClick: false
|
||||
initalFormParams: {}
|
||||
initalFormParamsIgnore: false
|
||||
|
||||
events:
|
||||
'submit form': 'submit'
|
||||
|
@ -746,10 +748,10 @@ class App.ControllerModal extends App.Controller
|
|||
centerButtons: @centerButtons
|
||||
leftButtons: @leftButtons
|
||||
)
|
||||
modal.find('.modal-body').html content
|
||||
modal.find('.modal-body').html(content)
|
||||
if !@initRenderingDone
|
||||
@initRenderingDone = true
|
||||
@html modal
|
||||
@html(modal)
|
||||
else
|
||||
@$('.modal-dialog').replaceWith(modal)
|
||||
@post()
|
||||
|
@ -761,6 +763,8 @@ class App.ControllerModal extends App.Controller
|
|||
@el
|
||||
|
||||
render: =>
|
||||
@initalFormParamsIgnore = false
|
||||
|
||||
if @buttonSubmit is true
|
||||
@buttonSubmit = 'Submit'
|
||||
if @buttonCancel is true
|
||||
|
@ -775,19 +779,18 @@ class App.ControllerModal extends App.Controller
|
|||
if @small
|
||||
@el.addClass('modal--small')
|
||||
|
||||
@el.modal
|
||||
@el.modal(
|
||||
keyboard: @keyboard
|
||||
show: true
|
||||
backdrop: @backdrop
|
||||
container: @container
|
||||
.on
|
||||
'show.bs.modal': @onShow
|
||||
'shown.bs.modal': @onShown
|
||||
'hide.bs.modal': @onClose
|
||||
'hidden.bs.modal': =>
|
||||
@onClosed()
|
||||
$('.modal').remove()
|
||||
'dismiss.bs.modal': @onCancel
|
||||
).on(
|
||||
'show.bs.modal': @localOnShow
|
||||
'shown.bs.modal': @localOnShown
|
||||
'hide.bs.modal': @localOnClose
|
||||
'hidden.bs.modal': @localOnClosed
|
||||
'dismiss.bs.modal': @localOnCancel
|
||||
)
|
||||
|
||||
if @closeOnAnyClick
|
||||
@el.on('click', =>
|
||||
|
@ -797,6 +800,7 @@ class App.ControllerModal extends App.Controller
|
|||
close: (e) =>
|
||||
if e
|
||||
e.preventDefault()
|
||||
@initalFormParamsIgnore = true
|
||||
@el.modal('hide')
|
||||
|
||||
formParams: =>
|
||||
|
@ -804,28 +808,50 @@ class App.ControllerModal extends App.Controller
|
|||
return @formParam(@container.find('.modal form'))
|
||||
return @formParam(@$('.modal form'))
|
||||
|
||||
onShow: ->
|
||||
localOnShow: (e) =>
|
||||
@onShow(e)
|
||||
|
||||
onShow: (e) ->
|
||||
# do nothing
|
||||
|
||||
onShown: =>
|
||||
localOnShown: (e) =>
|
||||
@onShown(e)
|
||||
|
||||
onShown: (e) =>
|
||||
@$('input:not([disabled]):not([type="hidden"]):not(".btn"), textarea').first().focus()
|
||||
@initalFormParams = @formParams()
|
||||
|
||||
localOnClose: (e) =>
|
||||
diff = difference(@initalFormParams, @formParams())
|
||||
if @initalFormParamsIgnore is false && !_.isEmpty(diff)
|
||||
if !confirm(App.i18n.translateContent('The form content has been changed. Do you want to close it and lose your changes?'))
|
||||
e.preventDefault()
|
||||
return
|
||||
@onClose(e)
|
||||
|
||||
onClose: ->
|
||||
# do nothing
|
||||
|
||||
onClosed: ->
|
||||
localOnClosed: (e) =>
|
||||
@onClosed(e)
|
||||
$('.modal').remove()
|
||||
|
||||
onClosed: (e) ->
|
||||
# do nothing
|
||||
|
||||
onSubmit: ->
|
||||
# do nothing
|
||||
localOnCancel: (e) =>
|
||||
@onCancel(e)
|
||||
|
||||
onCancel: ->
|
||||
onCancel: (e) ->
|
||||
# do nothing
|
||||
|
||||
cancel: (e) =>
|
||||
@close(e)
|
||||
@onCancel(e)
|
||||
|
||||
onSubmit: (e) ->
|
||||
# do nothing
|
||||
|
||||
submit: (e) =>
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
|
|
|
@ -86,6 +86,9 @@ class App.ControllerForm extends App.Controller
|
|||
for attribute in @attributes
|
||||
attribute_count = attribute_count + 1
|
||||
|
||||
if @isDisabled == true
|
||||
attribute.disabled = true
|
||||
|
||||
# add item
|
||||
item = @formGenItem(attribute, className, fieldset, attribute_count)
|
||||
item.appendTo(fieldset)
|
||||
|
|
|
@ -148,24 +148,26 @@ class App.ControllerGenericIndex extends App.Controller
|
|||
return item
|
||||
)
|
||||
|
||||
# show description button, only if content exists
|
||||
showDescription = false
|
||||
if App[ @genericObject ].description && !_.isEmpty(objects)
|
||||
showDescription = true
|
||||
if !@table
|
||||
|
||||
@html App.view('generic/admin/index')(
|
||||
head: @pageData.objects
|
||||
notes: @pageData.notes
|
||||
buttons: @pageData.buttons
|
||||
menus: @pageData.menus
|
||||
showDescription: showDescription
|
||||
)
|
||||
# show description button, only if content exists
|
||||
showDescription = false
|
||||
if App[ @genericObject ].description && !_.isEmpty(objects)
|
||||
showDescription = true
|
||||
|
||||
# show description in content if no no content exists
|
||||
if _.isEmpty(objects) && App[ @genericObject ].description
|
||||
description = marked(App[ @genericObject ].description)
|
||||
@$('.table-overview').html(description)
|
||||
return
|
||||
@html App.view('generic/admin/index')(
|
||||
head: @pageData.objects
|
||||
notes: @pageData.notes
|
||||
buttons: @pageData.buttons
|
||||
menus: @pageData.menus
|
||||
showDescription: showDescription
|
||||
)
|
||||
|
||||
# show description in content if no no content exists
|
||||
if _.isEmpty(objects) && App[ @genericObject ].description
|
||||
description = marked(App[ @genericObject ].description)
|
||||
@$('.table-overview').html(description)
|
||||
return
|
||||
|
||||
# append content table
|
||||
params = _.extend(
|
||||
|
@ -184,7 +186,10 @@ class App.ControllerGenericIndex extends App.Controller
|
|||
},
|
||||
@pageData.tableExtend
|
||||
)
|
||||
new App.ControllerTable(params)
|
||||
if !@table
|
||||
@table = new App.ControllerTable(params)
|
||||
else
|
||||
@table.update(objects: objects)
|
||||
|
||||
edit: (id, e) =>
|
||||
e.preventDefault()
|
||||
|
@ -651,7 +656,7 @@ class App.Sidebar extends App.Controller
|
|||
'.sidebar': 'sidebars'
|
||||
|
||||
events:
|
||||
'click .tabsSidebar-tab': 'toggleTab'
|
||||
'click .tabsSidebar-tab': 'toggleTab'
|
||||
'click .tabsSidebar-close': 'toggleSidebar'
|
||||
'click .sidebar-header .js-headline': 'toggleDropdown'
|
||||
|
||||
|
@ -675,26 +680,48 @@ class App.Sidebar extends App.Controller
|
|||
@toggleTabAction(name)
|
||||
|
||||
render: =>
|
||||
itemsLocal = []
|
||||
for item in @items
|
||||
itemLocal = item.sidebarItem()
|
||||
if itemLocal
|
||||
itemsLocal.push itemLocal
|
||||
|
||||
# container
|
||||
localEl = $(App.view('generic/sidebar_tabs')(
|
||||
items: @items
|
||||
items: itemsLocal
|
||||
scrollbarWidth: App.Utils.getScrollBarWidth()
|
||||
dir: App.i18n.dir()
|
||||
))
|
||||
|
||||
# init content callback
|
||||
for item in @items
|
||||
area = localEl.filter('.sidebar[data-tab="' + item.name + '"]')
|
||||
if item.callback
|
||||
item.callback( area.find('.sidebar-content') )
|
||||
if item.actions
|
||||
new App.ActionRow(
|
||||
el: area.find('.js-actions')
|
||||
items: item.actions
|
||||
type: 'small'
|
||||
)
|
||||
# init sidebar badget
|
||||
for item in itemsLocal
|
||||
el = localEl.find('.tabsSidebar-tab[data-tab="' + item.name + '"]')
|
||||
if item.badgeCallback
|
||||
item.badgeCallback(el)
|
||||
else
|
||||
@badgeRender(el, item)
|
||||
|
||||
# init sidebar content
|
||||
for item in itemsLocal
|
||||
if item.sidebarCallback
|
||||
el = localEl.filter('.sidebar[data-tab="' + item.name + '"]')
|
||||
item.sidebarCallback(el.find('.sidebar-content'))
|
||||
if !_.isEmpty(item.sidebarActions)
|
||||
new App.ActionRow(
|
||||
el: el.find('.js-actions')
|
||||
items: item.sidebarActions
|
||||
type: 'small'
|
||||
)
|
||||
|
||||
@html localEl
|
||||
|
||||
badgeRender: (el, item) =>
|
||||
@badgeEl = el
|
||||
@badgeRenderLocal(item)
|
||||
|
||||
badgeRenderLocal: (item) =>
|
||||
@badgeEl.html(App.view('generic/sidebar_tabs_item')(icon: item.badgeIcon))
|
||||
|
||||
toggleDropdown: (e) ->
|
||||
e.stopPropagation()
|
||||
$(e.currentTarget).next('.js-actions').find('.dropdown-toggle').dropdown('toggle')
|
||||
|
@ -1170,7 +1197,6 @@ class App.ObserverController extends App.Controller
|
|||
if @globalRerender
|
||||
@bind('ui:rerender', =>
|
||||
@lastAttributres = undefined
|
||||
console.log('aaaa', @model, @template)
|
||||
@maybeRender(App[@model].fullLocal(@object_id))
|
||||
)
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ class App.ControllerTable extends App.Controller
|
|||
checkBoxColWidth: 40
|
||||
radioColWidth: 22
|
||||
sortableColWidth: 36
|
||||
destroyColWidth: 70
|
||||
|
||||
elements:
|
||||
'.js-tableHead': 'tableHead'
|
||||
|
@ -133,6 +134,8 @@ class App.ControllerTable extends App.Controller
|
|||
customOrderDirection: undefined
|
||||
customOrderBy: undefined
|
||||
|
||||
frontendTimeUpdateExecute: true
|
||||
|
||||
bindCol: {}
|
||||
bindRow: {}
|
||||
|
||||
|
@ -269,6 +272,7 @@ class App.ControllerTable extends App.Controller
|
|||
@currentRows = newCurrentRows
|
||||
@log 'debug', 'table.fullRender.contentRemoved', removePositions, addPositions
|
||||
@renderPager(@el, true)
|
||||
@frontendTimeUpdateElement(@el) if @frontendTimeUpdateExecute is true
|
||||
return ['fullRender.contentRemoved', removePositions, addPositions]
|
||||
|
||||
if newRows.length isnt @currentRows.length
|
||||
|
@ -304,6 +308,7 @@ class App.ControllerTable extends App.Controller
|
|||
else
|
||||
@currentRows = clone(rows)
|
||||
container.find('.js-tableBody').html(rows)
|
||||
@frontendTimeUpdateElement(container) if @frontendTimeUpdateExecute is true
|
||||
|
||||
@renderPager(container)
|
||||
|
||||
|
@ -506,6 +511,7 @@ class App.ControllerTable extends App.Controller
|
|||
|
||||
# get header data
|
||||
@headers = []
|
||||
availableWidth = @availableWidth
|
||||
for item in @overviewAttributes
|
||||
headerFound = false
|
||||
for attributeName, attribute of @attributesList
|
||||
|
@ -520,7 +526,7 @@ class App.ControllerTable extends App.Controller
|
|||
# e.g. column: owner
|
||||
headerFound = true
|
||||
if @headerWidth[attribute.name]
|
||||
attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth
|
||||
attribute.displayWidth = @headerWidth[attribute.name] * availableWidth
|
||||
else if !attribute.width
|
||||
attribute.displayWidth = @baseColWidth
|
||||
else
|
||||
|
@ -529,7 +535,7 @@ class App.ControllerTable extends App.Controller
|
|||
unit = attribute.width.match(/[px|%]+/)[0]
|
||||
|
||||
if unit is '%'
|
||||
attribute.displayWidth = value / 100 * @el.width()
|
||||
attribute.displayWidth = value / 100 * availableWidth
|
||||
else
|
||||
attribute.displayWidth = value
|
||||
@headers.push attribute
|
||||
|
@ -538,7 +544,7 @@ class App.ControllerTable extends App.Controller
|
|||
if attributeName is "#{item}_id" || attributeName is "#{item}_ids"
|
||||
headerFound = true
|
||||
if @headerWidth[attribute.name]
|
||||
attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth
|
||||
attribute.displayWidth = @headerWidth[attribute.name] * availableWidth
|
||||
else if !attribute.width
|
||||
attribute.displayWidth = @baseColWidth
|
||||
else
|
||||
|
@ -547,7 +553,7 @@ class App.ControllerTable extends App.Controller
|
|||
unit = attribute.width.match(/[px|%]+/)[0]
|
||||
|
||||
if unit is '%'
|
||||
attribute.displayWidth = value / 100 * @el.width()
|
||||
attribute.displayWidth = value / 100 * availableWidth
|
||||
else
|
||||
attribute.displayWidth = value
|
||||
@headers.push attribute
|
||||
|
@ -741,8 +747,10 @@ class App.ControllerTable extends App.Controller
|
|||
if @availableWidth is 0
|
||||
@availableWidth = @minTableWidth
|
||||
|
||||
availableWidth = @availableWidth
|
||||
|
||||
widths = @getHeaderWidths()
|
||||
shrinkBy = Math.ceil (widths - @availableWidth) / @getShrinkableHeadersCount()
|
||||
shrinkBy = Math.ceil (widths - availableWidth) / @getShrinkableHeadersCount()
|
||||
|
||||
# make all cols evenly smaller
|
||||
@headers = _.map @headers, (col) =>
|
||||
|
@ -751,7 +759,8 @@ class App.ControllerTable extends App.Controller
|
|||
return col
|
||||
|
||||
# give left-over space from rounding to last column to get to 100%
|
||||
roundingLeftOver = @availableWidth - @getHeaderWidths()
|
||||
roundingLeftOver = availableWidth - @getHeaderWidths()
|
||||
|
||||
# but only if there is something left over (will get negative when there are too many columns for each column to stay in their min width)
|
||||
if roundingLeftOver > 0
|
||||
@headers[@headers.length - 1].displayWidth = @headers[@headers.length - 1].displayWidth + roundingLeftOver
|
||||
|
@ -777,6 +786,9 @@ class App.ControllerTable extends App.Controller
|
|||
if @dndCallback
|
||||
widths += @sortableColWidth
|
||||
|
||||
if @destroy
|
||||
widths += @destroyColWidth
|
||||
|
||||
widths
|
||||
|
||||
setHeaderWidths: =>
|
||||
|
|
|
@ -478,7 +478,6 @@ class App.ChannelEmailEdit extends App.ControllerModal
|
|||
class App.ChannelEmailAccountWizard extends App.WizardModal
|
||||
elements:
|
||||
'.modal-body': 'body'
|
||||
|
||||
events:
|
||||
'submit .js-intro': 'probeBasedOnIntro'
|
||||
'submit .js-inbound': 'probeInbound'
|
||||
|
@ -487,6 +486,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
'click .js-goToSlide': 'goToSlide'
|
||||
'click .js-expert': 'probeBasedOnIntro'
|
||||
'click .js-close': 'hide'
|
||||
inboundPassword: ''
|
||||
outboundPassword: ''
|
||||
passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
@ -503,9 +505,17 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
|
||||
if @channel
|
||||
@account =
|
||||
inbound: @channel.options.inbound
|
||||
outbound: @channel.options.outbound
|
||||
meta: {}
|
||||
inbound: clone(@channel.options.inbound)
|
||||
outbound: clone(@channel.options.outbound)
|
||||
meta: {}
|
||||
|
||||
# remember passwords, do not show in ui
|
||||
if @account.inbound.options && @account.inbound.options.password
|
||||
@inboundPassword = @account.inbound.options.password
|
||||
@account.inbound.options.password = @passwordPlaceholder
|
||||
if @account.outbound.options && @account.outbound.options.password
|
||||
@outboundPassword = @account.outbound.options.password
|
||||
@account.outbound.options.password = @passwordPlaceholder
|
||||
|
||||
if @container
|
||||
@el.addClass('modal--local')
|
||||
|
@ -515,17 +525,17 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
if @channel
|
||||
@$('.js-goToSlide[data-slide=js-intro]').addClass('hidden')
|
||||
|
||||
@el.modal
|
||||
@el.modal(
|
||||
keyboard: true
|
||||
show: true
|
||||
backdrop: true
|
||||
container: @container
|
||||
.on
|
||||
).on(
|
||||
'hidden.bs.modal': =>
|
||||
if @callback
|
||||
@callback()
|
||||
@el.remove()
|
||||
|
||||
)
|
||||
if @slide
|
||||
@showSlide(@slide)
|
||||
|
||||
|
@ -712,6 +722,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
# get params
|
||||
params = @formParam(e.target)
|
||||
|
||||
if params.options.password is @passwordPlaceholder
|
||||
params.options.password = @inboundPassword
|
||||
|
||||
# let backend know about the channel
|
||||
if @channel
|
||||
params.channel_id = @channel.id
|
||||
|
@ -771,6 +784,9 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
params = @formParam(e.target)
|
||||
params['email'] = @account['meta']['email']
|
||||
|
||||
if params.options.password is @passwordPlaceholder
|
||||
params.options.password = @outboundPassword
|
||||
|
||||
if !params['email'] && @channel
|
||||
email_addresses = App.EmailAddress.search(filter: { channel_id: @channel.id })
|
||||
if email_addresses && email_addresses[0]
|
||||
|
@ -867,11 +883,13 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
class App.ChannelEmailNotificationWizard extends App.WizardModal
|
||||
elements:
|
||||
'.modal-body': 'body'
|
||||
|
||||
events:
|
||||
'change .js-outbound [name=adapter]': 'toggleOutboundAdapter'
|
||||
'submit .js-outbound': 'probleOutbound'
|
||||
'click .js-close': 'hide'
|
||||
inboundPassword: ''
|
||||
outboundPassword: ''
|
||||
passwordPlaceholder: '{{{{{{{{{{{{SECRTE_PASSWORD}}}}}}}}}}}}'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
|
@ -888,27 +906,35 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
|
|||
|
||||
if @channel
|
||||
@account =
|
||||
inbound: @channel.options.inbound
|
||||
outbound: @channel.options.outbound
|
||||
inbound: clone(@channel.options.inbound)
|
||||
outbound: clone(@channel.options.outbound)
|
||||
|
||||
# remember passwords, do not show in ui
|
||||
if @account.inbound && @account.inbound.options && @account.inbound.options.password
|
||||
@inboundPassword = @account.inbound.options.password
|
||||
@account.inbound.options.password = @passwordPlaceholder
|
||||
if @account.outbound && @account.outbound.options && @account.outbound.options.password
|
||||
@outboundPassword = @account.outbound.options.password
|
||||
@account.outbound.options.password = @passwordPlaceholder
|
||||
|
||||
if @container
|
||||
@el.addClass('modal--local')
|
||||
|
||||
@render()
|
||||
|
||||
@el.modal
|
||||
@el.modal(
|
||||
keyboard: true
|
||||
show: true
|
||||
backdrop: true
|
||||
container: @container
|
||||
.on
|
||||
).on(
|
||||
'show.bs.modal': @onShow
|
||||
'shown.bs.modal': @onShown
|
||||
'hidden.bs.modal': =>
|
||||
if @callback
|
||||
@callback()
|
||||
@el.remove()
|
||||
|
||||
)
|
||||
if @slide
|
||||
@showSlide(@slide)
|
||||
|
||||
|
@ -956,6 +982,9 @@ class App.ChannelEmailNotificationWizard extends App.WizardModal
|
|||
# get params
|
||||
params = @formParam(e.target)
|
||||
|
||||
if params.options && params.options.password is @passwordPlaceholder
|
||||
params.options.password = @outboundPassword
|
||||
|
||||
# let backend know about the channel
|
||||
params.channel_id = @channel.id
|
||||
|
||||
|
|
|
@ -67,11 +67,15 @@ class App.ChannelForm extends App.ControllerSubContent
|
|||
# rebuild preview
|
||||
params.test = true
|
||||
if params.modal
|
||||
@$('.js-modal').removeClass('hide')
|
||||
@$('.js-inlineForm').addClass('hide')
|
||||
@$('.js-formInline').addClass('hide')
|
||||
@$('.js-formBtn').removeClass('hide')
|
||||
@$('.js-formBtn').ZammadForm(params)
|
||||
@$('.js-formBtn').text('Feedback')
|
||||
else
|
||||
@$('.js-modal').addClass('hide')
|
||||
@$('.js-inlineForm').removeClass('hide')
|
||||
@$('.js-formBtn').addClass('hide')
|
||||
@$('.js-formInline').removeClass('hide')
|
||||
@$('.js-formInline').ZammadForm(params)
|
||||
|
|
|
@ -131,13 +131,12 @@ class Form extends App.Controller
|
|||
if _.isEmpty(job)
|
||||
@lastImport.html('')
|
||||
return
|
||||
countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped + job.result.failed
|
||||
if !job.result.roles
|
||||
job.result.roles = {}
|
||||
for role_id, statistic of job.result.role_ids
|
||||
role = App.Role.find(role_id)
|
||||
job.result.roles[role.displayName()] = statistic
|
||||
el = $(App.view('integration/exchange_last_import')(job: job, countDone: countDone))
|
||||
el = $(App.view('integration/exchange_last_import')(job: job))
|
||||
@lastImport.html(el)
|
||||
|
||||
activeDryRun: =>
|
||||
|
@ -540,34 +539,26 @@ class ConnectionWizard extends App.WizardModal
|
|||
@showAlert('js-error', (job.result.error || job.result.info))
|
||||
return
|
||||
|
||||
total = 0
|
||||
if job.result && _.keys(job.result).length > 0
|
||||
@$('.js-preprogress').addClass('hide')
|
||||
@$('.js-analyzing').removeClass('hide')
|
||||
|
||||
analized = 0
|
||||
total = job.result.sum
|
||||
for action, sum of job.result
|
||||
continue if action == 'folders'
|
||||
continue if action == 'sum'
|
||||
analized += sum
|
||||
|
||||
@$('.js-progress progress').attr('value', analized)
|
||||
@$('.js-progress progress').attr('max', total)
|
||||
@$('.js-progress progress').attr('value', job.result.sum)
|
||||
@$('.js-progress progress').attr('max', job.result.total)
|
||||
|
||||
if job.finished_at
|
||||
# reset initial state in case the back button is used
|
||||
@$('.js-preprogress').removeClass('hide')
|
||||
@$('.js-analyzing').addClass('hide')
|
||||
|
||||
@tryResult(job, total)
|
||||
@tryResult(job)
|
||||
else
|
||||
@delay(@tryLoop, 4000)
|
||||
)
|
||||
|
||||
tryResult: (job, total) =>
|
||||
tryResult: (job) =>
|
||||
@showSlide('js-try')
|
||||
el = $(App.view('integration/exchange_summary')(job: job, countDone: total))
|
||||
el = $(App.view('integration/exchange_summary')(job: job))
|
||||
@el.find('.js-summary').html(el)
|
||||
|
||||
App.Config.set(
|
||||
|
|
|
@ -132,13 +132,12 @@ class Form extends App.Controller
|
|||
if _.isEmpty(job)
|
||||
@lastImport.html('')
|
||||
return
|
||||
countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped + job.result.failed
|
||||
if !job.result.roles
|
||||
job.result.roles = {}
|
||||
for role_id, statistic of job.result.role_ids
|
||||
role = App.Role.find(role_id)
|
||||
job.result.roles[role.displayName()] = statistic
|
||||
el = $(App.view('integration/ldap_last_import')(job: job, countDone: countDone))
|
||||
el = $(App.view('integration/ldap_last_import')(job: job))
|
||||
@lastImport.html(el)
|
||||
|
||||
activeDryRun: =>
|
||||
|
@ -419,7 +418,7 @@ class ConnectionWizard extends App.WizardModal
|
|||
if !_.isArray(user_attributes[key])
|
||||
user_attributes[key] = [user_attributes[key]]
|
||||
user_attributes_local =
|
||||
"#{@wizardConfig['user_uid']}": 'login'
|
||||
'samaccountname': 'login'
|
||||
length = user_attributes.source.length-1
|
||||
for count in [0..length]
|
||||
if user_attributes.source[count] && user_attributes.dest[count]
|
||||
|
@ -450,7 +449,7 @@ class ConnectionWizard extends App.WizardModal
|
|||
buildRowsUserMap: (user_attribute_map) =>
|
||||
|
||||
# show static login row
|
||||
userUidDisplayValue = @wizardConfig.wizardData.backend_user_attributes[ @wizardConfig['user_uid'] ]
|
||||
userUidDisplayValue = @wizardConfig.wizardData.backend_user_attributes['samaccountname']
|
||||
|
||||
el = [
|
||||
$(App.view('integration/ldap_user_attribute_row_read_only')(
|
||||
|
@ -459,7 +458,7 @@ class ConnectionWizard extends App.WizardModal
|
|||
))
|
||||
]
|
||||
for source, dest of user_attribute_map
|
||||
continue if source == @wizardConfig['user_uid']
|
||||
continue if source == 'samaccountname'
|
||||
continue if !(source of @wizardConfig.wizardData.backend_user_attributes)
|
||||
el.push @buildRowUserAttribute(source, dest)
|
||||
el
|
||||
|
@ -539,22 +538,12 @@ class ConnectionWizard extends App.WizardModal
|
|||
@showAlert('js-error', (job.result.error || job.result.info))
|
||||
return
|
||||
|
||||
if job.result && job.result.sum
|
||||
if job.result && job.result.total
|
||||
@$('.js-preprogress').addClass('hide')
|
||||
@$('.js-analyzing').removeClass('hide')
|
||||
total = 0
|
||||
if job.result.created
|
||||
total += job.result.created
|
||||
if job.result.failed
|
||||
total += job.result.failed
|
||||
if job.result.skipped
|
||||
total += job.result.skipped
|
||||
if job.result.unchanged
|
||||
total += job.result.unchanged
|
||||
if job.result.updated
|
||||
total += job.result.updated
|
||||
@$('.js-progress progress').attr('value', total)
|
||||
@$('.js-progress progress').attr('max', job.result.sum)
|
||||
|
||||
@$('.js-progress progress').attr('value', job.result.sum)
|
||||
@$('.js-progress progress').attr('max', job.result.total)
|
||||
if job.finished_at
|
||||
# reset initial state in case the back button is used
|
||||
@$('.js-preprogress').removeClass('hide')
|
||||
|
@ -574,9 +563,8 @@ class ConnectionWizard extends App.WizardModal
|
|||
for role_id, statistic of job.result.role_ids
|
||||
role = App.Role.find(role_id)
|
||||
job.result.roles[role.displayName()] = statistic
|
||||
countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped
|
||||
@showSlide('js-try')
|
||||
el = $(App.view('integration/ldap_summary')(job: job, countDone: countDone))
|
||||
el = $(App.view('integration/ldap_summary')(job: job))
|
||||
@el.find('.js-summary').html(el)
|
||||
|
||||
App.Config.set(
|
||||
|
|
|
@ -32,7 +32,7 @@ class App.SettingsAreaItem extends App.Controller
|
|||
)
|
||||
|
||||
new App.ControllerForm(
|
||||
el: @el.find('.form-item'),
|
||||
el: @el.find('.form-item')
|
||||
model: { configure_attributes: @configure_attributes, className: '' }
|
||||
autofocus: false
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -42,7 +42,7 @@ class App.SettingsAreaTicketNumber extends App.Controller
|
|||
number = "#{App.Config.get('ticket_hook')}#{App.Config.get('system_id')}"
|
||||
counter = '1'
|
||||
if paramsItem.min_size
|
||||
minSize = parseInt(paramsItem.min_size)
|
||||
minSize = parseInt(paramsItem.min_size) - "#{App.Config.get('system_id')}".length
|
||||
if paramsItem.checksum
|
||||
minSize -= 1
|
||||
if minSize > 1
|
||||
|
|
|
@ -6,24 +6,24 @@ class App.UiElement.column_select extends App.UiElement.ApplicationUiElement
|
|||
attribute.multiple = 'multiple'
|
||||
|
||||
# build options list based on config
|
||||
@getConfigOptionList( attribute, params )
|
||||
@getConfigOptionList(attribute, params)
|
||||
|
||||
# build options list based on relation
|
||||
@getRelationOptionList( attribute, params )
|
||||
@getRelationOptionList(attribute, params)
|
||||
|
||||
# add null selection if needed
|
||||
@addNullOption( attribute, params )
|
||||
@addNullOption(attribute, params)
|
||||
|
||||
# sort attribute.options
|
||||
@sortOptions( attribute, params )
|
||||
@sortOptions(attribute, params)
|
||||
|
||||
# find selected/checked item of list
|
||||
@selectedOptions( attribute, params )
|
||||
@selectedOptions(attribute, params)
|
||||
|
||||
# disable item of list
|
||||
@disabledOptions( attribute, params )
|
||||
@disabledOptions(attribute, params)
|
||||
|
||||
# filter attributes
|
||||
@filterOption( attribute, params )
|
||||
@filterOption(attribute, params)
|
||||
|
||||
new App.ColumnSelect( attribute: attribute ).element()
|
||||
new App.ColumnSelect(attribute: attribute).element()
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.richtext
|
||||
@render: (attribute) ->
|
||||
|
||||
item = $( App.view('generic/richtext')( attribute: attribute ) )
|
||||
@render: (attribute, params) ->
|
||||
item = $( App.view('generic/richtext')(attribute: attribute) )
|
||||
item.find('[contenteditable]').ce(
|
||||
mode: attribute.type
|
||||
maxlength: attribute.maxlength
|
||||
|
@ -15,42 +14,42 @@ class App.UiElement.richtext
|
|||
new App[plugin.controller](params)
|
||||
|
||||
if attribute.upload
|
||||
item.append( $( App.view('generic/attachment')( attribute: attribute ) ) )
|
||||
@attachments = []
|
||||
item.append( $( App.view('generic/attachment')(attribute: attribute) ) )
|
||||
|
||||
renderAttachment = (file) =>
|
||||
item.find('.attachments').append( App.view('generic/attachment_item')(
|
||||
fileName: file.filename
|
||||
fileSize: App.Utils.humanFileSize(file.size)
|
||||
store_id: file.store_id
|
||||
))
|
||||
item.on(
|
||||
'click'
|
||||
"[data-id=#{file.store_id}]", (e) =>
|
||||
@attachments = _.filter(
|
||||
@attachments,
|
||||
(item) ->
|
||||
return if item.id isnt file.store_id
|
||||
item
|
||||
)
|
||||
store_id = $(e.currentTarget).data('id')
|
||||
renderFile = (file) =>
|
||||
item.find('.attachments').append(App.view('generic/attachment_item')(file))
|
||||
@attachments.push file
|
||||
|
||||
# delete attachment from storage
|
||||
App.Ajax.request(
|
||||
type: 'DELETE'
|
||||
url: "#{App.Config.get('api_path')}/ticket_attachment_upload"
|
||||
data: JSON.stringify(store_id: store_id),
|
||||
processData: false
|
||||
)
|
||||
if params && params.attachments
|
||||
for file in params.attachments
|
||||
renderFile(file)
|
||||
|
||||
# remove attachment from dom
|
||||
element = $(e.currentTarget).closest('.attachments')
|
||||
$(e.currentTarget).closest('.attachment').remove()
|
||||
# empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o
|
||||
if element.find('.attachment').length == 0
|
||||
element.empty()
|
||||
# remove items
|
||||
item.find('.attachments').on('click', '.js-delete', (e) =>
|
||||
id = $(e.currentTarget).data('id')
|
||||
@attachments = _.filter(
|
||||
@attachments,
|
||||
(item) ->
|
||||
return if item.id.toString() is id.toString()
|
||||
item
|
||||
)
|
||||
|
||||
@attachments = []
|
||||
# delete attachment from storage
|
||||
App.Ajax.request(
|
||||
type: 'DELETE'
|
||||
url: "#{App.Config.get('api_path')}/ticket_attachment_upload"
|
||||
data: JSON.stringify(id: id),
|
||||
processData: false
|
||||
)
|
||||
|
||||
# remove attachment from dom
|
||||
element = $(e.currentTarget).closest('.attachments')
|
||||
$(e.currentTarget).closest('.attachment').remove()
|
||||
if element.find('.attachment').length == 0
|
||||
element.empty()
|
||||
)
|
||||
|
||||
@progressBar = item.find('.attachmentUpload-progressBar')
|
||||
@progressText = item.find('.js-percentage')
|
||||
@attachmentPlaceholder = item.find('.attachmentPlaceholder')
|
||||
|
@ -84,7 +83,6 @@ class App.UiElement.richtext
|
|||
# Called after received response from the server
|
||||
onCompleted: (response) =>
|
||||
response = JSON.parse(response)
|
||||
@attachments.push response.data
|
||||
|
||||
@attachmentPlaceholder.removeClass('hide')
|
||||
@attachmentUpload.addClass('hide')
|
||||
|
@ -93,7 +91,7 @@ class App.UiElement.richtext
|
|||
@progressBar.width(parseInt(0) + '%')
|
||||
@progressText.text('')
|
||||
|
||||
renderAttachment(response.data)
|
||||
renderFile(response.data)
|
||||
item.find('input').val('')
|
||||
|
||||
App.Log.debug 'UiElement.richtext', 'upload complete', response.data
|
||||
|
@ -111,4 +109,5 @@ class App.UiElement.richtext
|
|||
)
|
||||
)
|
||||
App.Delay.set(u, 100, undefined, 'form_upload')
|
||||
|
||||
item
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# coffeelint: disable=camel_case_classes
|
||||
class App.UiElement.richtext_search
|
||||
@render: (attribute) ->
|
||||
$( App.view('generic/input')( attribute: attribute ) )
|
|
@ -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) )
|
|
@ -22,9 +22,12 @@ class App.UiElement.ticket_selector
|
|||
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
|
||||
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)']
|
||||
'boolean$': ['is', 'is not']
|
||||
'integer$': ['is', 'is not']
|
||||
'^radio$': ['is', 'is not']
|
||||
'^select$': ['is', 'is not']
|
||||
'^tree_select$': ['is', 'is not']
|
||||
'^input$': ['contains', 'contains not']
|
||||
'^richtext$': ['contains', 'contains not']
|
||||
'^textarea$': ['contains', 'contains not']
|
||||
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']
|
||||
|
||||
|
@ -34,9 +37,12 @@ class App.UiElement.ticket_selector
|
|||
'^timestamp$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed']
|
||||
'^date$': ['before (absolute)', 'after (absolute)', 'before (relative)', 'after (relative)', 'within next (relative)', 'within last (relative)', 'has changed']
|
||||
'boolean$': ['is', 'is not', 'has changed']
|
||||
'integer$': ['is', 'is not', 'has changed']
|
||||
'^radio$': ['is', 'is not', 'has changed']
|
||||
'^select$': ['is', 'is not', 'has changed']
|
||||
'^tree_select$': ['is', 'is not', 'has changed']
|
||||
'^input$': ['contains', 'contains not', 'has changed']
|
||||
'^richtext$': ['contains', 'contains not', 'has changed']
|
||||
'^textarea$': ['contains', 'contains not', 'has changed']
|
||||
'^tag$': ['contains all', 'contains one', 'contains all not', 'contains one not']
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ class App.TicketCreate extends App.Controller
|
|||
# define default type
|
||||
@default_type = 'phone-in'
|
||||
|
||||
@formId = App.ControllerForm.formId()
|
||||
|
||||
# remember split info if exists
|
||||
@split = ''
|
||||
if @ticket_id && @article_id
|
||||
|
@ -92,6 +94,10 @@ class App.TicketCreate extends App.Controller
|
|||
else
|
||||
@$('[name="cc"]').closest('.form-group').addClass('hide')
|
||||
|
||||
# show notice
|
||||
@$('.js-note').addClass('hide')
|
||||
@$(".js-note[data-type='#{type}']").removeClass('hide')
|
||||
|
||||
App.TaskManager.touch(@task_key)
|
||||
|
||||
meta: =>
|
||||
|
@ -158,7 +164,7 @@ class App.TicketCreate extends App.Controller
|
|||
# get data / in case also ticket data for split
|
||||
buildScreen: (params) =>
|
||||
|
||||
if !params.ticket_id && !params.article_id
|
||||
if _.isEmpty(params.ticket_id) && _.isEmpty(params.article_id)
|
||||
if !_.isEmpty(params.customer_id)
|
||||
@render(options: { customer_id: params.customer_id })
|
||||
return
|
||||
|
@ -173,6 +179,7 @@ class App.TicketCreate extends App.Controller
|
|||
data:
|
||||
ticket_id: params.ticket_id
|
||||
article_id: params.article_id
|
||||
form_id: @formId
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
|
||||
|
@ -194,6 +201,9 @@ class App.TicketCreate extends App.Controller
|
|||
else
|
||||
t.body = App.Utils.text2html(a.body)
|
||||
|
||||
# add attachments
|
||||
t.attachments = data.attachments
|
||||
|
||||
# render page
|
||||
@render(options: t)
|
||||
)
|
||||
|
@ -201,23 +211,20 @@ class App.TicketCreate extends App.Controller
|
|||
render: (template = {}) ->
|
||||
|
||||
# get params
|
||||
params = {}
|
||||
params = @prefilledParams || {}
|
||||
if template && !_.isEmpty(template.options)
|
||||
params = template.options
|
||||
else if App.TaskManager.get(@task_key) && !_.isEmpty(App.TaskManager.get(@task_key).state)
|
||||
params = App.TaskManager.get(@task_key).state
|
||||
if !_.isEmpty(params['form_id'])
|
||||
@formId = params['form_id']
|
||||
|
||||
if params['form_id']
|
||||
@form_id = params['form_id']
|
||||
else
|
||||
@form_id = App.ControllerForm.formId()
|
||||
|
||||
@html App.view('agent_ticket_create')(
|
||||
@html(App.view('agent_ticket_create')(
|
||||
head: 'New Ticket'
|
||||
agent: @permissionCheck('ticket.agent')
|
||||
admin: @permissionCheck('admin')
|
||||
form_id: @form_id
|
||||
)
|
||||
form_id: @formId
|
||||
))
|
||||
|
||||
signatureChanges = (params, attribute, attributes, classname, form, ui) =>
|
||||
if attribute && attribute.name is 'group_id'
|
||||
|
@ -272,7 +279,7 @@ class App.TicketCreate extends App.Controller
|
|||
}
|
||||
new App.ControllerForm(
|
||||
el: @$('.ticket-form-top')
|
||||
form_id: @form_id
|
||||
form_id: @formId
|
||||
model: App.Ticket
|
||||
screen: 'create_top'
|
||||
events:
|
||||
|
@ -288,14 +295,14 @@ class App.TicketCreate extends App.Controller
|
|||
|
||||
new App.ControllerForm(
|
||||
el: @$('.article-form-top')
|
||||
form_id: @form_id
|
||||
form_id: @formId
|
||||
model: App.TicketArticle
|
||||
screen: 'create_top'
|
||||
params: params
|
||||
)
|
||||
new App.ControllerForm(
|
||||
el: @$('.ticket-form-middle')
|
||||
form_id: @form_id
|
||||
form_id: @formId
|
||||
model: App.Ticket
|
||||
screen: 'create_middle'
|
||||
events:
|
||||
|
@ -310,7 +317,7 @@ class App.TicketCreate extends App.Controller
|
|||
)
|
||||
new App.ControllerForm(
|
||||
el: @$('.ticket-form-bottom')
|
||||
form_id: @form_id
|
||||
form_id: @formId
|
||||
model: App.Ticket
|
||||
screen: 'create_bottom'
|
||||
events:
|
||||
|
@ -420,7 +427,7 @@ class App.TicketCreate extends App.Controller
|
|||
body: params.body
|
||||
type_id: type.id
|
||||
sender_id: sender.id
|
||||
form_id: @form_id
|
||||
form_id: @formId
|
||||
content_type: 'text/html'
|
||||
}
|
||||
else
|
||||
|
@ -432,7 +439,7 @@ class App.TicketCreate extends App.Controller
|
|||
body: params.body
|
||||
type_id: type.id
|
||||
sender_id: sender.id
|
||||
form_id: @form_id
|
||||
form_id: @formId
|
||||
content_type: 'text/html'
|
||||
}
|
||||
|
||||
|
|
|
@ -32,9 +32,7 @@ class App.TicketCreateSidebar extends App.Controller
|
|||
params: @params
|
||||
query: @query
|
||||
)
|
||||
item = @sidebarBackends[key].sidebarItem()
|
||||
if item
|
||||
@sidebarItems.push item
|
||||
@sidebarItems.push @sidebarBackends[key]
|
||||
|
||||
new App.Sidebar(
|
||||
el: @el
|
||||
|
|
|
@ -1,26 +1,62 @@
|
|||
class SidebarCustomer extends App.Controller
|
||||
sidebarItem: =>
|
||||
return if !@permissionCheck('ticket.agent')
|
||||
return if !@params.customer_id
|
||||
{
|
||||
head: 'Customer'
|
||||
name: 'customer'
|
||||
icon: 'person'
|
||||
actions: [
|
||||
return if _.isEmpty(@params.customer_id)
|
||||
@item = {
|
||||
name: 'customer'
|
||||
badgeCallback: @badgeRender
|
||||
sidebarHead: 'Customer'
|
||||
sidebarCallback: @showCustomer
|
||||
sidebarActions: [
|
||||
{
|
||||
title: 'Edit Customer'
|
||||
name: 'customer-edit'
|
||||
callback: @editCustomer
|
||||
},
|
||||
]
|
||||
callback: @showCustomer
|
||||
}
|
||||
|
||||
metaBadge: (user) =>
|
||||
counter = ''
|
||||
cssClass = ''
|
||||
counter = @sidebarItemCounter(user)
|
||||
|
||||
if @Config.get('ui_sidebar_open_ticket_indicator_colored') is true
|
||||
if counter == 1
|
||||
cssClass = 'tabsSidebar-tab-count--warning'
|
||||
if counter > 1
|
||||
cssClass = 'tabsSidebar-tab-count--danger'
|
||||
|
||||
{
|
||||
name: 'customer'
|
||||
icon: 'person'
|
||||
counterPossible: true
|
||||
counter: counter
|
||||
cssClass: cssClass
|
||||
}
|
||||
|
||||
badgeRender: (el) =>
|
||||
@badgeEl = el
|
||||
if App.User.exists(@params.customer_id)
|
||||
user = App.User.find(@params.customer_id)
|
||||
@badgeRenderLocal(user)
|
||||
|
||||
badgeRenderLocal: (user) =>
|
||||
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge(user)))
|
||||
|
||||
sidebarItemCounter: (user) ->
|
||||
counter = ''
|
||||
if user && user.preferences && user.preferences.tickets_open
|
||||
counter = user.preferences.tickets_open
|
||||
counter
|
||||
|
||||
showCustomer: (el) =>
|
||||
@el = el
|
||||
@elSidebar = el
|
||||
return if _.isEmpty(@params.customer_id)
|
||||
new App.WidgetUser(
|
||||
el: @el
|
||||
el: @elSidebar
|
||||
user_id: @params.customer_id
|
||||
callback: @badgeRenderLocal
|
||||
)
|
||||
|
||||
editCustomer: =>
|
||||
|
@ -32,7 +68,7 @@ class SidebarCustomer extends App.Controller
|
|||
title: 'Users'
|
||||
object: 'User'
|
||||
objects: 'Users'
|
||||
container: @el.closest('.content')
|
||||
container: @elSidebar.closest('.content')
|
||||
)
|
||||
|
||||
App.Config.set('200-Customer', SidebarCustomer, 'TicketCreateSidebar')
|
||||
|
|
|
@ -6,24 +6,25 @@ class SidebarOrganization extends App.Controller
|
|||
customer = App.User.find(@params.customer_id)
|
||||
@organization_id = customer.organization_id
|
||||
return if !@organization_id
|
||||
{
|
||||
head: 'Organization'
|
||||
@item = {
|
||||
name: 'organization'
|
||||
icon: 'group'
|
||||
actions: [
|
||||
badgeIcon: 'group'
|
||||
sidebarHead: 'Organization'
|
||||
sidebarCallback: @showOrganization
|
||||
sidebarActions: [
|
||||
{
|
||||
title: 'Edit Organization'
|
||||
name: 'organization-edit'
|
||||
callback: @editOrganization
|
||||
},
|
||||
]
|
||||
callback: @showOrganization
|
||||
}
|
||||
@item
|
||||
|
||||
showOrganization: (el) =>
|
||||
@el = el
|
||||
@elSidebar = el
|
||||
new App.WidgetOrganization(
|
||||
el: @el
|
||||
el: @elSidebar
|
||||
organization_id: @organization_id
|
||||
)
|
||||
|
||||
|
@ -35,7 +36,7 @@ class SidebarOrganization extends App.Controller
|
|||
title: 'Organizations'
|
||||
object: 'Organization'
|
||||
objects: 'Organizations'
|
||||
container: @el.closest('.content')
|
||||
container: @elSidebar.closest('.content')
|
||||
)
|
||||
|
||||
App.Config.set('300-Organization', SidebarOrganization, 'TicketCreateSidebar')
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
class SidebarTemplate extends App.Controller
|
||||
sidebarItem: =>
|
||||
return if !@permissionCheck('ticket.agent')
|
||||
{
|
||||
head: 'Templates'
|
||||
name: 'template'
|
||||
icon: 'templates'
|
||||
actions: []
|
||||
callback: @showTemplates
|
||||
@item = {
|
||||
name: 'template'
|
||||
badgeIcon: 'templates'
|
||||
badgeCallback: @badgeRender
|
||||
sidebarHead: 'Templates'
|
||||
sidebarActions: []
|
||||
sidebarCallback: @showTemplates
|
||||
}
|
||||
@item
|
||||
|
||||
showTemplates: (el) =>
|
||||
@el = el
|
||||
|
|
|
@ -35,7 +35,7 @@ class App.CustomerChat extends App.Controller
|
|||
active_agent_ids: []
|
||||
|
||||
@render()
|
||||
@on 'layout-has-changed', @propagateLayoutChange
|
||||
@on('layout-has-changed', @propagateLayoutChange)
|
||||
|
||||
# update navbar on new status
|
||||
@bind('chat_status_agent', (data) =>
|
||||
|
@ -163,6 +163,12 @@ class App.CustomerChat extends App.Controller
|
|||
@title 'Customer Chat', true
|
||||
@navupdate '#customer_chat'
|
||||
|
||||
if params.session_id
|
||||
callback = (session) =>
|
||||
@addChat(session)
|
||||
App.ChatSession.full(params.session_id, callback)
|
||||
@navigate '#customer_chat'
|
||||
|
||||
active: (state) =>
|
||||
return @shown if state is undefined
|
||||
@shown = state
|
||||
|
@ -264,10 +270,11 @@ class App.CustomerChat extends App.Controller
|
|||
|
||||
addChat: (session) ->
|
||||
return if @chatWindows[session.session_id]
|
||||
chat = new ChatWindow
|
||||
chat = new ChatWindow(
|
||||
session: session
|
||||
removeCallback: @removeChat
|
||||
messageCallback: @updateNavMenu
|
||||
)
|
||||
|
||||
@workspace.append chat.el
|
||||
chat.render()
|
||||
|
@ -289,7 +296,7 @@ class App.CustomerChat extends App.Controller
|
|||
propagateLayoutChange: (event) =>
|
||||
# adjust scroll position on layoutChange
|
||||
for session_id, chat of @chatWindows
|
||||
chat.trigger 'layout-changed'
|
||||
chat.trigger('layout-changed')
|
||||
|
||||
acceptChat: =>
|
||||
return if @windowCount() >= @maxChatWindows
|
||||
|
@ -324,19 +331,6 @@ class App.CustomerChat extends App.Controller
|
|||
currentPosition: =>
|
||||
@$('.main').scrollTop()
|
||||
|
||||
class CustomerChatRouter extends App.ControllerPermanent
|
||||
requiredPermission: 'chat.agent'
|
||||
constructor: (params) ->
|
||||
super
|
||||
|
||||
App.TaskManager.execute(
|
||||
key: 'CustomerChat'
|
||||
controller: 'CustomerChat'
|
||||
params: {}
|
||||
show: true
|
||||
persistent: true
|
||||
)
|
||||
|
||||
class ChatWindow extends App.Controller
|
||||
className: 'chat-window'
|
||||
|
||||
|
@ -348,6 +342,9 @@ class ChatWindow extends App.Controller
|
|||
'click .js-close': 'close'
|
||||
'click .js-disconnect': 'disconnect'
|
||||
'click .js-scrollHint': 'onScrollHintClick'
|
||||
'click .js-info': 'toggleMeta'
|
||||
'click .js-createTicket': 'ticketCreate'
|
||||
'submit .js-metaForm': 'sendMetaForm'
|
||||
|
||||
elements:
|
||||
'.js-customerChatInput': 'input'
|
||||
|
@ -355,8 +352,11 @@ class ChatWindow extends App.Controller
|
|||
'.js-close': 'closeButton'
|
||||
'.js-disconnect': 'disconnectButton'
|
||||
'.js-body': 'body'
|
||||
'.js-meta': 'meta'
|
||||
'.js-name': 'metaName'
|
||||
'.js-scrollHolder': 'scrollHolder'
|
||||
'.js-scrollHint': 'scrollHint'
|
||||
'.js-metaForm': 'metaForm'
|
||||
|
||||
sounds:
|
||||
message: new Audio('assets/sounds/chat_message.mp3')
|
||||
|
@ -374,9 +374,11 @@ class ChatWindow extends App.Controller
|
|||
@scrollSnapTolerance = 10 # pixels
|
||||
|
||||
@chat = App.Chat.find(@session.chat_id)
|
||||
@name = "#{@chat.displayName()} ##{@session.id}"
|
||||
@name = @chat.displayName()
|
||||
if @session && !_.isEmpty(@session.name)
|
||||
@name = @session.name
|
||||
|
||||
@on 'layout-change', @onLayoutChange
|
||||
@on('layout-change', @onLayoutChange)
|
||||
|
||||
@bind('chat_session_typing', (data) =>
|
||||
return if data.session_id isnt @session.session_id
|
||||
|
@ -413,12 +415,45 @@ class ChatWindow extends App.Controller
|
|||
onLayoutChange: =>
|
||||
@scrollToBottom()
|
||||
|
||||
render: ->
|
||||
@html App.view('customer_chat/chat_window')
|
||||
name: @name
|
||||
toggleMeta: =>
|
||||
if @meta.hasClass('hidden')
|
||||
@showMeta()
|
||||
else
|
||||
@hideMeta()
|
||||
|
||||
@el.one 'transitionend', @onTransitionend
|
||||
@scrollHolder.scroll @detectScrolledtoBottom
|
||||
hideMeta: =>
|
||||
@body.removeClass('hidden')
|
||||
@meta.addClass('hidden')
|
||||
@sendMetaForm()
|
||||
|
||||
showMeta: =>
|
||||
@body.addClass('hidden')
|
||||
@meta.removeClass('hidden')
|
||||
|
||||
sendMetaForm: (e) =>
|
||||
if e
|
||||
e.preventDefault()
|
||||
params = @formParam(@metaForm)
|
||||
|
||||
App.WebSocket.send(
|
||||
event:'chat_session_update'
|
||||
data:
|
||||
session_id: @session.session_id
|
||||
name: params.name
|
||||
tags: params.tags
|
||||
)
|
||||
|
||||
if !_.isEmpty(params.name)
|
||||
@metaName.text(params.name)
|
||||
|
||||
render: ->
|
||||
@html App.view('customer_chat/chat_window')(
|
||||
name: @name
|
||||
session: @session
|
||||
)
|
||||
|
||||
@el.one('transitionend', @onTransitionend)
|
||||
@scrollHolder.scroll(@detectScrolledtoBottom)
|
||||
|
||||
# force repaint
|
||||
@el.prop('offsetHeight')
|
||||
|
@ -426,18 +461,24 @@ class ChatWindow extends App.Controller
|
|||
|
||||
# @addMessage 'Hello. My name is Roger, how can I help you?', 'agent'
|
||||
if @session
|
||||
|
||||
# set chat to offline if state is already closed
|
||||
activeChat = true
|
||||
if @session.state is 'closed'
|
||||
activeChat = false
|
||||
|
||||
if @session && @session.preferences && @session.preferences.url
|
||||
@addNoticeMessage(@session.preferences.url)
|
||||
@addNoticeMessage(@session.preferences.url, undefined, activeChat)
|
||||
|
||||
if @session.messages
|
||||
for message in @session.messages
|
||||
if message.created_by_id
|
||||
@addMessage message.content, 'agent'
|
||||
@addMessage(message.content, 'agent', false, activeChat)
|
||||
else
|
||||
@addMessage message.content, 'customer'
|
||||
@addMessage(message.content, 'customer', false, activeChat)
|
||||
|
||||
# send init reply
|
||||
if !@session.messages || _.isEmpty(@session.messages)
|
||||
if activeChat && _.isEmpty(@session.messages)
|
||||
preferences = @Session.get('preferences')
|
||||
if preferences.chat && preferences.chat.phrase
|
||||
phrases = preferences.chat.phrase[@session.chat_id]
|
||||
|
@ -447,20 +488,9 @@ class ChatWindow extends App.Controller
|
|||
@input.html(phrase)
|
||||
@sendMessage(1600)
|
||||
|
||||
@$('.js-info').popover(
|
||||
trigger: 'hover'
|
||||
html: true
|
||||
animation: false
|
||||
delay: 0
|
||||
placement: 'bottom'
|
||||
container: 'body' # place in body do prevent it from animating
|
||||
title: ->
|
||||
App.i18n.translateContent('Details')
|
||||
content: =>
|
||||
App.view('customer_chat/chat_window_info')(
|
||||
session: @session
|
||||
)
|
||||
)
|
||||
# set chat to offline if state is already closed
|
||||
if !activeChat
|
||||
@goOffline()
|
||||
|
||||
# show text module UI
|
||||
new App.WidgetTextModule(
|
||||
|
@ -470,6 +500,18 @@ class ChatWindow extends App.Controller
|
|||
config: App.Config.all()
|
||||
)
|
||||
|
||||
configureAttributesOutbound = [
|
||||
{ name: 'name', display: 'Name', tag: 'input', null: true, },
|
||||
{ name: 'tags', display: 'Tags', tag: 'tag', null: true, },
|
||||
]
|
||||
new App.ControllerForm(
|
||||
el: @$('.js-metaForm')
|
||||
model:
|
||||
configure_attributes: configureAttributesOutbound
|
||||
className: ''
|
||||
params: @session
|
||||
)
|
||||
|
||||
focus: =>
|
||||
@input.focus()
|
||||
|
||||
|
@ -498,7 +540,8 @@ class ChatWindow extends App.Controller
|
|||
@goOffline()
|
||||
|
||||
close: =>
|
||||
@el.one 'transitionend', { callback: @release }, @onTransitionend
|
||||
@sendMetaForm()
|
||||
@el.one('transitionend', { callback: @release }, @onTransitionend)
|
||||
@el.removeClass('is-open')
|
||||
if @removeCallback
|
||||
@removeCallback(@session.session_id)
|
||||
|
@ -577,7 +620,8 @@ class ChatWindow extends App.Controller
|
|||
)
|
||||
@delay(send, delay)
|
||||
|
||||
@addMessage content, 'agent'
|
||||
@hideMeta()
|
||||
@addMessage(content, 'agent')
|
||||
@input.html('')
|
||||
|
||||
updateModified: (state) =>
|
||||
|
@ -614,18 +658,19 @@ class ChatWindow extends App.Controller
|
|||
@messageCallback(@session.session_id)
|
||||
@unreadMessagesCounter = 0
|
||||
|
||||
addMessage: (message, sender, isNew) =>
|
||||
@maybeAddTimestamp()
|
||||
addMessage: (message, sender, isNew, useMaybeAddTimestamp = true) =>
|
||||
@maybeAddTimestamp() if useMaybeAddTimestamp
|
||||
|
||||
@lastAddedType = sender
|
||||
|
||||
@body.append App.view('customer_chat/chat_message')
|
||||
@body.append App.view('customer_chat/chat_message')(
|
||||
message: message
|
||||
sender: sender
|
||||
isNew: isNew
|
||||
timestamp: Date.now()
|
||||
)
|
||||
|
||||
@scrollToBottom showHint: true
|
||||
@scrollToBottom(showHint: true)
|
||||
|
||||
showWritingLoader: =>
|
||||
if !@isTyping
|
||||
|
@ -667,33 +712,37 @@ class ChatWindow extends App.Controller
|
|||
@lastAddedType = 'timestamp'
|
||||
|
||||
addTimestamp: (label, time) =>
|
||||
@body.append App.view('customer_chat/chat_timestamp')
|
||||
@body.append App.view('customer_chat/chat_timestamp')(
|
||||
label: label
|
||||
time: time
|
||||
)
|
||||
|
||||
updateLastTimestamp: (label, time) ->
|
||||
@body
|
||||
.find('.js-timestamp')
|
||||
.last()
|
||||
.replaceWith App.view('customer_chat/chat_timestamp')
|
||||
.replaceWith App.view('customer_chat/chat_timestamp')(
|
||||
label: label
|
||||
time: time
|
||||
)
|
||||
|
||||
addStatusMessage: (message, args) ->
|
||||
@maybeAddTimestamp()
|
||||
addStatusMessage: (message, args, useMaybeAddTimestamp = true) ->
|
||||
@maybeAddTimestamp() if useMaybeAddTimestamp
|
||||
|
||||
@body.append App.view('customer_chat/chat_status_message')
|
||||
@body.append App.view('customer_chat/chat_status_message')(
|
||||
message: message
|
||||
args: args
|
||||
)
|
||||
|
||||
@scrollToBottom()
|
||||
|
||||
addNoticeMessage: (message, args) ->
|
||||
@maybeAddTimestamp()
|
||||
addNoticeMessage: (message, args, useMaybeAddTimestamp = true) ->
|
||||
@maybeAddTimestamp() if useMaybeAddTimestamp
|
||||
|
||||
@body.append App.view('customer_chat/chat_notice_message')
|
||||
@body.append App.view('customer_chat/chat_notice_message')(
|
||||
message: message
|
||||
args: args
|
||||
)
|
||||
|
||||
@scrollToBottom()
|
||||
|
||||
|
@ -717,6 +766,37 @@ class ChatWindow extends App.Controller
|
|||
else if showHint
|
||||
@showScrollHint()
|
||||
|
||||
ticketCreate: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
id = Math.floor( Math.random() * 99999 )
|
||||
@navigate "#ticket/create/id/#{id}"
|
||||
|
||||
# cleanup params
|
||||
fqdn = App.Config.get('fqdn')
|
||||
http_type = App.Config.get('http_type')
|
||||
url = ''
|
||||
session = @session
|
||||
|
||||
# in case we do not have a model, create one
|
||||
if session && !session.uiUrl
|
||||
session = new App.ChatSession(session)
|
||||
if session && session.uiUrl
|
||||
url = session.uiUrl()
|
||||
|
||||
clean_params =
|
||||
id: id
|
||||
prefilledParams:
|
||||
body: "#{http_type}://#{fqdn}/#{url}"
|
||||
title: 'Chat'
|
||||
|
||||
App.TaskManager.execute(
|
||||
key: "TicketCreateScreen-#{id}"
|
||||
controller: 'TicketCreate'
|
||||
params: clean_params
|
||||
show: true
|
||||
)
|
||||
|
||||
class Setting extends App.ControllerModal
|
||||
buttonClose: true
|
||||
buttonCancel: true
|
||||
|
@ -784,6 +864,24 @@ class Setting extends App.ControllerModal
|
|||
msg: App.i18n.translateContent(data.message)
|
||||
)
|
||||
|
||||
class CustomerChatRouter extends App.ControllerPermanent
|
||||
requiredPermission: 'chat.agent'
|
||||
constructor: (params) ->
|
||||
super
|
||||
|
||||
# cleanup params
|
||||
clean_params =
|
||||
session_id: params.session_id
|
||||
|
||||
App.TaskManager.execute(
|
||||
key: 'CustomerChat'
|
||||
controller: 'CustomerChat'
|
||||
params: clean_params
|
||||
show: true
|
||||
persistent: true
|
||||
)
|
||||
|
||||
App.Config.set('customer_chat', CustomerChatRouter, 'Routes')
|
||||
App.Config.set('customer_chat/session/:session_id', CustomerChatRouter, 'Routes')
|
||||
App.Config.set('CustomerChat', { controller: 'CustomerChat', permission: ['chat.agent'] }, 'permanentTask')
|
||||
App.Config.set('CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', shown: false, permission: ['chat.agent'], class: 'chat' }, 'NavBar')
|
||||
|
|
|
@ -337,11 +337,9 @@ class Base extends App.WizardFullScreen
|
|||
@hideAlerts()
|
||||
@disable(e)
|
||||
|
||||
# get params
|
||||
@params = @formParam(e.target)
|
||||
|
||||
# add logo
|
||||
@params.logo = @logoPreview.attr('src')
|
||||
@params.locale_default = App.i18n.detectBrowserLocale()
|
||||
|
||||
store = (logoResizeDataUrl) =>
|
||||
@params.logo_resize = logoResizeDataUrl
|
||||
|
@ -354,7 +352,7 @@ class Base extends App.WizardFullScreen
|
|||
success: (data, status, xhr) =>
|
||||
if data.result is 'ok'
|
||||
for key, value of data.settings
|
||||
App.Config.set( key, value )
|
||||
App.Config.set(key, value)
|
||||
if App.Config.get('system_online_service')
|
||||
@navigate 'getting_started/channel/email_pre_configured'
|
||||
else
|
||||
|
|
|
@ -3,6 +3,7 @@ class App.IdoitObjectSelector extends App.ControllerModal
|
|||
buttonCancel: true
|
||||
buttonSubmit: true
|
||||
head: 'i-doit'
|
||||
lastSearchTermEmpty: false
|
||||
|
||||
content: ->
|
||||
@ajax(
|
||||
|
@ -44,16 +45,24 @@ class App.IdoitObjectSelector extends App.ControllerModal
|
|||
''
|
||||
|
||||
search: (filter) =>
|
||||
if _.isEmpty(filter.type) && _.isEmpty(filter.title)
|
||||
@lastSearchTermEmpty = true
|
||||
@renderResult()
|
||||
return
|
||||
if _.isEmpty(filter.type)
|
||||
delete filter.type
|
||||
if _.isEmpty(filter.title)
|
||||
delete filter.title
|
||||
else
|
||||
filter.title = "%#{filter.title}%"
|
||||
@lastSearchTermEmpty = false
|
||||
@ajax(
|
||||
id: 'idoit-object-selector'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/integration/idoit"
|
||||
data: JSON.stringify(method: 'cmdb.objects', filter: filter)
|
||||
success: (data, status, xhr) =>
|
||||
return if @lastSearchTermEmpty
|
||||
@renderResult(data.response.result)
|
||||
|
||||
error: (xhr, status, error) =>
|
||||
|
|
|
@ -154,35 +154,35 @@ class Index extends App.ControllerContent
|
|||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
|
||||
if data.result is 'import_done'
|
||||
window.location.reload()
|
||||
return
|
||||
|
||||
if data.result is 'error'
|
||||
@$('.js-error').removeClass('hide')
|
||||
@$('.js-error').html(App.i18n.translateContent(data.message))
|
||||
else
|
||||
@$('.js-error').addClass('hide')
|
||||
|
||||
if data.message is 'not running' && @updateMigrationDisplayLoop > 16
|
||||
if _.isEmpty(data.result) && @updateMigrationDisplayLoop > 16
|
||||
@$('.js-error').removeClass('hide')
|
||||
@$('.js-error').html(App.i18n.translateContent('Background process did not start or has not finished! Please contact your support.'))
|
||||
return
|
||||
|
||||
if data.result is 'in_progress'
|
||||
for key, item of data.data
|
||||
if item.done > item.total
|
||||
item.done = item.total
|
||||
if !_.isEmpty(data.result['error'])
|
||||
@$('.js-error').removeClass('hide')
|
||||
@$('.js-error').html(App.i18n.translateContent(data.result['error']))
|
||||
else
|
||||
@$('.js-error').addClass('hide')
|
||||
|
||||
if key == 'Ticket' && item.total >= 1000
|
||||
if !_.isEmpty(data.finished_at) && _.isEmpty(data.result['error'])
|
||||
window.location.reload()
|
||||
return
|
||||
|
||||
if !_.isEmpty(data.result)
|
||||
for model, stats of data.result
|
||||
if stats.sum > stats.total
|
||||
stats.sum = stats.total
|
||||
|
||||
if model == 'Ticket' && stats.total >= 1000
|
||||
@ticketCountInfo.removeClass('hide')
|
||||
|
||||
element = @$('.js-' + key.toLowerCase() )
|
||||
element.find('.js-done').text(item.done)
|
||||
element.find('.js-total').text(item.total)
|
||||
element.find('progress').attr('max', item.total )
|
||||
element.find('progress').attr('value', item.done )
|
||||
if item.total <= item.done
|
||||
element = @$('.js-' + model.toLowerCase() )
|
||||
element.find('.js-done').text(stats.sum)
|
||||
element.find('.js-total').text(stats.total)
|
||||
element.find('progress').attr('max', stats.total )
|
||||
element.find('progress').attr('value', stats.sum )
|
||||
if stats.total <= stats.sum
|
||||
element.addClass('is-done')
|
||||
else
|
||||
element.removeClass('is-done')
|
||||
|
|
|
@ -349,7 +349,7 @@ class LayoutRefCommunicationReply extends App.ControllerContent
|
|||
|
||||
file = @uploadQueue.shift()
|
||||
# console.log "working of", file, "from", @uploadQueue
|
||||
@fakeUpload file.name, file.size, @workOfUploadQueue
|
||||
@fakeUpload(file.name, file.size, @workOfUploadQueue)
|
||||
|
||||
humanFileSize: (size) ->
|
||||
i = Math.floor( Math.log(size) / Math.log(1024) )
|
||||
|
@ -363,27 +363,27 @@ class LayoutRefCommunicationReply extends App.ControllerContent
|
|||
@attachmentPlaceholder.removeClass('hide')
|
||||
@attachmentUpload.addClass('hide')
|
||||
|
||||
fakeUpload: (fileName, fileSize, callback) ->
|
||||
fakeUpload: (filename, size, callback) ->
|
||||
@attachmentPlaceholder.addClass('hide')
|
||||
@attachmentUpload.removeClass('hide')
|
||||
|
||||
progress = 0
|
||||
duration = fileSize / 1024
|
||||
duration = size / 1024
|
||||
|
||||
for i in [0..100]
|
||||
setTimeout @updateUploadProgress, i*duration/100 , i
|
||||
|
||||
setTimeout (=>
|
||||
callback()
|
||||
@renderAttachment(fileName, fileSize)
|
||||
@renderAttachment(filename, size)
|
||||
), duration
|
||||
|
||||
renderAttachment: (fileName, fileSize) =>
|
||||
@attachments.push([fileName, fileSize])
|
||||
@attachmentsHolder.append App.view('generic/attachment_item')
|
||||
fileName: fileName
|
||||
fileSize: @humanFileSize(fileSize)
|
||||
|
||||
renderAttachment: (filename, size) =>
|
||||
@attachments.push([filename, size])
|
||||
@attachmentsHolder.append(App.view('generic/attachment_item')
|
||||
filename: filename
|
||||
size: @humanFileSize(size)
|
||||
)
|
||||
|
||||
App.Config.set( 'layout_ref/communication_reply/:content', LayoutRefCommunicationReply, 'Routes' )
|
||||
|
||||
|
@ -2121,7 +2121,7 @@ class TwitterConversationRef extends App.ControllerContent
|
|||
open: 88
|
||||
closed: 20
|
||||
|
||||
maxTextLength: 140
|
||||
maxTextLength: 280
|
||||
warningTextLength: 10
|
||||
|
||||
constructor: ->
|
||||
|
|
|
@ -23,17 +23,22 @@ class Index extends App.ControllerSubContent
|
|||
]
|
||||
container: @el.closest('.content')
|
||||
large: true
|
||||
dndCallback: =>
|
||||
dndCallback: (e, item) =>
|
||||
items = @el.find('table > tbody > tr')
|
||||
order = []
|
||||
prios = []
|
||||
prio = 0
|
||||
for item in items
|
||||
prio += 1
|
||||
id = $(item).data('id')
|
||||
overview = App.Overview.find(id)
|
||||
if overview.prio isnt prio
|
||||
overview.prio = prio
|
||||
overview.save()
|
||||
prios.push [id, prio]
|
||||
|
||||
@ajax(
|
||||
id: 'overview_prio'
|
||||
type: 'POST'
|
||||
url: "#{@apiPath}/overviews_prio"
|
||||
processData: true
|
||||
data: JSON.stringify(prios: prios)
|
||||
)
|
||||
)
|
||||
|
||||
App.Config.set('Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, permission: ['admin.overview'] }, 'NavBarAdmin')
|
||||
|
|
|
@ -108,33 +108,40 @@ class Graph extends App.ControllerContent
|
|||
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
update: (data) =>
|
||||
|
||||
update = (data) =>
|
||||
# show only selected lines
|
||||
dataNew = {}
|
||||
for key, value of data.data
|
||||
if @params.backendSelected[key] is true
|
||||
dataNew[key] = value
|
||||
@ui.storeParams()
|
||||
|
||||
# show only selected lines
|
||||
dataNew = {}
|
||||
for key, value of data.data
|
||||
if @params.backendSelected[key] is true
|
||||
dataNew[key] = value
|
||||
@ui.storeParams()
|
||||
if !@lastNewData
|
||||
@lastNewData = {}
|
||||
|
||||
if !@lastNewData
|
||||
@lastNewData = {}
|
||||
return if @lastNewData && JSON.stringify(dataNew) is JSON.stringify(@lastNewData)
|
||||
@lastNewData = dataNew
|
||||
|
||||
return if @lastNewData && JSON.stringify(dataNew) is JSON.stringify(@lastNewData)
|
||||
@lastNewData = dataNew
|
||||
|
||||
@draw(dataNew)
|
||||
t = new Date
|
||||
@el.find('#download-chart').html(t.toString())
|
||||
new Download(
|
||||
@draw(dataNew)
|
||||
t = new Date
|
||||
@el.find('#download-chart').html(t.toString())
|
||||
if @downloadWidget
|
||||
@downloadWidget.update(
|
||||
config: @config
|
||||
params: @params
|
||||
ui: @ui
|
||||
)
|
||||
else
|
||||
@downloadWidget = new Download(
|
||||
el: @el.find('.js-dataDownload')
|
||||
config: @config
|
||||
params: @params
|
||||
ui: @ui
|
||||
)
|
||||
|
||||
render: =>
|
||||
|
||||
url = "#{@apiPath}/reports/generate"
|
||||
interval = 5 * 60000
|
||||
if @params.timeRange is 'year'
|
||||
|
@ -142,9 +149,9 @@ class Graph extends App.ControllerContent
|
|||
if @params.timeRange is 'month'
|
||||
interval = 60000
|
||||
if @params.timeRange is 'week'
|
||||
interval = 40000
|
||||
interval = 50000
|
||||
if @params.timeRange is 'day'
|
||||
interval = 20000
|
||||
interval = 30000
|
||||
if @params.timeRange is 'realtime'
|
||||
interval = 10000
|
||||
|
||||
|
@ -164,7 +171,7 @@ class Graph extends App.ControllerContent
|
|||
)
|
||||
processData: true
|
||||
success: (data) =>
|
||||
update(data)
|
||||
@update(data)
|
||||
@delay(@render, interval, 'report-update', 'page')
|
||||
)
|
||||
|
||||
|
@ -215,7 +222,7 @@ class Graph extends App.ControllerContent
|
|||
|
||||
class Download extends App.Controller
|
||||
events:
|
||||
'click .js-dataDownloadBackendSelector': 'tableUpdate'
|
||||
'click .js-dataDownloadBackendSelector': 'selectBackend'
|
||||
|
||||
constructor: (data) ->
|
||||
|
||||
|
@ -225,7 +232,24 @@ class Download extends App.Controller
|
|||
super
|
||||
@render()
|
||||
|
||||
render: ->
|
||||
selectBackend: (e) =>
|
||||
e.preventDefault()
|
||||
@el.find('.js-dataDownloadBackendSelector').parent().removeClass('active')
|
||||
$(e.target).parent().addClass('active')
|
||||
@profileSelectedId = $(e.target).data('profile-id')
|
||||
@params.downloadBackendSelected = $(e.target).data('backend')
|
||||
@ui.storeParams()
|
||||
@table = false
|
||||
@render()
|
||||
|
||||
update: =>
|
||||
@render()
|
||||
|
||||
render: =>
|
||||
|
||||
if !@contentRendered
|
||||
@contentRendered = true
|
||||
@html(App.view('report/download_content')())
|
||||
|
||||
reports = []
|
||||
|
||||
|
@ -244,44 +268,84 @@ class Download extends App.Controller
|
|||
@profileSelectedId = key
|
||||
profiles.push App.ReportProfile.find(key)
|
||||
|
||||
@html App.view('report/download_header')(
|
||||
downloadHeaderHtml = App.view('report/download_header')(
|
||||
reports: reports
|
||||
profiles: profiles
|
||||
downloadBackendSelected: @params.downloadBackendSelected
|
||||
metric: @config.metric[@params.metric]
|
||||
)
|
||||
if downloadHeaderHtml isnt @downloadHeaderHtml
|
||||
@el.find('.js-dataDownloadHeader').html(downloadHeaderHtml)
|
||||
@downloadHeaderHtml = downloadHeaderHtml
|
||||
|
||||
@tableUpdate()
|
||||
|
||||
tableUpdate: (e) =>
|
||||
if e
|
||||
e.preventDefault()
|
||||
@el.find('.js-dataDownloadBackendSelector').parent().removeClass('active')
|
||||
$(e.target).parent().addClass('active')
|
||||
@profileSelectedId = $(e.target).data('profile-id')
|
||||
@params.downloadBackendSelected = $(e.target).data('backend')
|
||||
@ui.storeParams()
|
||||
tableRender: (tickets, count) =>
|
||||
if _.isEmpty(tickets)
|
||||
@$('.js-dataDownloadButton').html('')
|
||||
@$('.js-dataDownloadTable').html('')
|
||||
return
|
||||
|
||||
table = (tickets, count) =>
|
||||
url = '#ticket/zoom/'
|
||||
if App.Config.get('import_mode')
|
||||
url = App.Config.get('import_otrs_endpoint') + '/index.pl?Action=AgentTicketZoom;TicketID='
|
||||
if _.isEmpty(tickets)
|
||||
@el.find('.js-dataDownloadTable').html('')
|
||||
else
|
||||
profile_id = 0
|
||||
for key, value of @params.profileSelected
|
||||
if value
|
||||
profile_id = key
|
||||
downloadUrl = "#{@apiPath}/reports/sets?sheet=true;metric=#{@params.metric};year=#{@params.year};month=#{@params.month};week=#{@params.week};day=#{@params.day};timeRange=#{@params.timeRange};profile_id=#{profile_id};downloadBackendSelected=#{@params.downloadBackendSelected}"
|
||||
html = App.view('report/download_list')(
|
||||
tickets: tickets
|
||||
count: count
|
||||
url: url
|
||||
download: downloadUrl
|
||||
)
|
||||
@el.find('.js-dataDownloadTable').html(html)
|
||||
profile_id = 0
|
||||
for key, value of @params.profileSelected
|
||||
if value
|
||||
profile_id = key
|
||||
downloadUrl = "#{@apiPath}/reports/sets?sheet=true;metric=#{@params.metric};year=#{@params.year};month=#{@params.month};week=#{@params.week};day=#{@params.day};timeRange=#{@params.timeRange};profile_id=#{profile_id};downloadBackendSelected=#{@params.downloadBackendSelected}"
|
||||
@$('.js-dataDownloadButton').html(App.view('report/download_button')(
|
||||
count: count
|
||||
downloadUrl: downloadUrl
|
||||
))
|
||||
|
||||
openTicket = (id,e) =>
|
||||
ticket = App.Ticket.findNative(id)
|
||||
@navigate ticket.uiUrl()
|
||||
callbackTicketTitleAdd = (value, object, attribute, attributes) ->
|
||||
attribute.title = object.title
|
||||
value
|
||||
callbackLinkToTicket = (value, object, attribute, attributes) ->
|
||||
attribute.link = object.uiUrl()
|
||||
value
|
||||
callbackIconHeader = (headers) ->
|
||||
attribute =
|
||||
name: 'icon'
|
||||
display: ''
|
||||
translation: false
|
||||
width: '28px'
|
||||
displayWidth:28
|
||||
unresizable: true
|
||||
headers.unshift(0)
|
||||
headers[0] = attribute
|
||||
headers
|
||||
callbackIcon = (value, object, attribute, header) ->
|
||||
value = ' '
|
||||
attribute.class = object.iconClass()
|
||||
attribute.link = ''
|
||||
attribute.title = object.iconTitle()
|
||||
value
|
||||
|
||||
params =
|
||||
el: @el.find('.js-dataDownloadTable')
|
||||
model: App.Ticket
|
||||
objects: tickets
|
||||
overviewAttributes: ['number', 'title', 'state', 'group', 'created_at']
|
||||
bindRow:
|
||||
events:
|
||||
'click': openTicket
|
||||
callbackHeader: [ callbackIconHeader ]
|
||||
callbackAttributes:
|
||||
icon:
|
||||
[ callbackIcon ]
|
||||
title:
|
||||
[ callbackLinkToTicket, callbackTicketTitleAdd ]
|
||||
number:
|
||||
[ callbackLinkToTicket, callbackTicketTitleAdd ]
|
||||
|
||||
if !@table
|
||||
@table = new App.ControllerTable(params)
|
||||
else
|
||||
@table.update(objects: tickets)
|
||||
|
||||
tableUpdate: =>
|
||||
@ajax(
|
||||
id: 'report_download'
|
||||
type: 'POST'
|
||||
|
@ -298,15 +362,14 @@ class Download extends App.Controller
|
|||
downloadBackendSelected: @params.downloadBackendSelected
|
||||
)
|
||||
processData: true
|
||||
success: (data) ->
|
||||
success: (data) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
ticket_collection = []
|
||||
if data.ticket_ids
|
||||
for record_id in data.ticket_ids
|
||||
ticket = App.Ticket.fullLocal( record_id )
|
||||
ticket = App.Ticket.fullLocal(record_id)
|
||||
ticket_collection.push ticket
|
||||
|
||||
table(ticket_collection, data.count)
|
||||
@tableRender(ticket_collection, data.count)
|
||||
)
|
||||
|
||||
class TimeRangePicker extends App.Controller
|
||||
|
|
|
@ -79,6 +79,7 @@ class App.Search extends App.Controller
|
|||
|
||||
@tabs = []
|
||||
for model in App.Config.get('models_searchable')
|
||||
model = model.replace(/::/, '')
|
||||
tab =
|
||||
name: model
|
||||
model: model
|
||||
|
|
|
@ -6,7 +6,7 @@ class App.TicketCustomer extends App.ControllerModal
|
|||
|
||||
content: ->
|
||||
configure_attributes = [
|
||||
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: true },
|
||||
{ name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organization/Company', minLengt: 2, disableCreateObject: false },
|
||||
]
|
||||
controller = new App.ControllerForm(
|
||||
model:
|
||||
|
@ -18,8 +18,19 @@ class App.TicketCustomer extends App.ControllerModal
|
|||
onSubmit: (e) =>
|
||||
params = @formParam(e.target)
|
||||
|
||||
@customer_id = params['customer_id']
|
||||
ticket = App.Ticket.find(@ticket_id)
|
||||
ticket.customer_id = params['customer_id']
|
||||
errors = ticket.validate()
|
||||
|
||||
if !_.isEmpty(errors)
|
||||
@log 'error', errors
|
||||
@formValidate(
|
||||
form: e.target
|
||||
errors: errors
|
||||
)
|
||||
return
|
||||
|
||||
@customer_id = params['customer_id']
|
||||
callback = =>
|
||||
|
||||
# close modal
|
||||
|
|
|
@ -956,7 +956,6 @@ class Table extends App.Controller
|
|||
ticketListShow = []
|
||||
for ticket in tickets
|
||||
ticketListShow.push App.Ticket.find(ticket.id)
|
||||
console.log('overview', overview)
|
||||
@overview = App.Overview.find(overview.id)
|
||||
@table.update(
|
||||
overviewAttributes: @overview.view.s
|
||||
|
|
|
@ -461,6 +461,7 @@ class App.TicketZoom extends App.Controller
|
|||
ui: @
|
||||
highligher: @highligher
|
||||
ticket_article_ids: @ticket_article_ids
|
||||
form_id: @form_id
|
||||
)
|
||||
|
||||
new App.TicketCustomerAvatar(
|
||||
|
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -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')
|
|
@ -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} "
|
||||
else
|
||||
articleNew.body = "#{recipientString} "
|
||||
|
||||
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')
|
|
@ -1,21 +1,13 @@
|
|||
class App.TicketZoomArticleActions extends App.Controller
|
||||
events:
|
||||
'click [data-type=public]': 'publicInternal'
|
||||
'click [data-type=internal]': 'publicInternal'
|
||||
'click [data-type=emailReply]': 'emailReply'
|
||||
'click [data-type=emailReplyAll]': 'emailReplyAll'
|
||||
'click [data-type=twitterStatusReply]': 'twitterStatusReply'
|
||||
'click [data-type=twitterDirectMessageReply]': 'twitterDirectMessageReply'
|
||||
'click [data-type=facebookFeedReply]': 'facebookFeedReply'
|
||||
'click [data-type=telegramPersonalMessageReply]': 'telegramPersonalMessageReply'
|
||||
'click [data-type=delete]': 'delete'
|
||||
'click .js-ArticleAction': 'actionPerform'
|
||||
|
||||
constructor: ->
|
||||
super
|
||||
@render()
|
||||
|
||||
render: ->
|
||||
actions = @actionRow(@article)
|
||||
actions = @actionRow(@ticket, @article)
|
||||
|
||||
if actions
|
||||
@html App.view('ticket_zoom/article_view_actions')(
|
||||
|
@ -25,371 +17,31 @@ class App.TicketZoomArticleActions extends App.Controller
|
|||
else
|
||||
@html ''
|
||||
|
||||
publicInternal: (e) =>
|
||||
e.preventDefault()
|
||||
articleContainer = $(e.target).closest('.ticket-article-item')
|
||||
article_id = $(e.target).parents('[data-id]').data('id')
|
||||
|
||||
# storage update
|
||||
article = App.TicketArticle.find(article_id)
|
||||
internal = true
|
||||
if article.internal == true
|
||||
internal = false
|
||||
@lastAttributres.internal = internal
|
||||
article.updateAttributes(internal: internal)
|
||||
|
||||
# runntime update
|
||||
if internal
|
||||
articleContainer.addClass('is-internal')
|
||||
else
|
||||
articleContainer.removeClass('is-internal')
|
||||
|
||||
@render()
|
||||
|
||||
actionRow: (article) ->
|
||||
if @permissionCheck('ticket.customer')
|
||||
return []
|
||||
|
||||
actionRow: (ticket, article) ->
|
||||
actionConfig = App.Config.get('TicketZoomArticleAction')
|
||||
keys = _.keys(actionConfig).sort()
|
||||
actions = []
|
||||
if article.internal is true
|
||||
actions = [
|
||||
{
|
||||
name: 'set to public'
|
||||
type: 'public'
|
||||
icon: 'lock-open'
|
||||
}
|
||||
]
|
||||
else
|
||||
actions = [
|
||||
{
|
||||
name: 'set to internal'
|
||||
type: 'internal'
|
||||
icon: 'lock'
|
||||
}
|
||||
]
|
||||
#if @article.type.name is 'note'
|
||||
# actions.push []
|
||||
group = @ticket.group
|
||||
if group.email_address_id && (article.type.name is 'email' || article.type.name is 'web')
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'emailReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
recipients = []
|
||||
if article.sender.name is 'Customer'
|
||||
if article.from
|
||||
localRecipients = emailAddresses.parseAddressList(article.from)
|
||||
if localRecipients
|
||||
recipients = recipients.concat localRecipients
|
||||
if article.to
|
||||
localRecipients = emailAddresses.parseAddressList(article.to)
|
||||
if localRecipients
|
||||
recipients = recipients.concat localRecipients
|
||||
if article.cc
|
||||
localRecipients = emailAddresses.parseAddressList(article.cc)
|
||||
if localRecipients
|
||||
recipients = recipients.concat localRecipients
|
||||
|
||||
# remove system addresses
|
||||
localAddresses = App.EmailAddress.all()
|
||||
forgeinRecipients = []
|
||||
recipientUsed = {}
|
||||
for recipient in recipients
|
||||
if !_.isEmpty(recipient.address)
|
||||
localRecipientAddress = recipient.address.toString().toLowerCase()
|
||||
if !recipientUsed[localRecipientAddress]
|
||||
recipientUsed[localRecipientAddress] = true
|
||||
localAddress = false
|
||||
for address in localAddresses
|
||||
if localRecipientAddress is address.email.toString().toLowerCase()
|
||||
recipientUsed[localRecipientAddress] = true
|
||||
localAddress = true
|
||||
if !localAddress
|
||||
forgeinRecipients.push recipient
|
||||
|
||||
# check if reply all is neede
|
||||
if forgeinRecipients.length > 1
|
||||
actions.push {
|
||||
name: 'reply all'
|
||||
type: 'emailReplyAll'
|
||||
icon: 'reply-all'
|
||||
href: '#'
|
||||
}
|
||||
if article.sender.name is 'Customer' && article.type.name is 'phone'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'emailReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
if article.sender.name is 'Agent' && article.type.name is 'phone'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'emailReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
if article.type.name is 'twitter status'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'twitterStatusReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
if article.type.name is 'twitter direct-message'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'twitterDirectMessageReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
if article.type.name is 'facebook feed post' || article.type.name is 'facebook feed comment'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'facebookFeedReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message'
|
||||
actions.push {
|
||||
name: 'reply'
|
||||
type: 'telegramPersonalMessageReply'
|
||||
icon: 'reply'
|
||||
href: '#'
|
||||
}
|
||||
|
||||
actions.push {
|
||||
name: 'split'
|
||||
type: 'split'
|
||||
icon: 'split'
|
||||
href: '#ticket/create/' + article.ticket_id + '/' + article.id
|
||||
}
|
||||
|
||||
if article.type.name is 'note'
|
||||
user = undefined
|
||||
if App.Session.get('id') == article.created_by_id
|
||||
user = App.User.find(App.Session.get('id'))
|
||||
if user.permission('ticket.agent')
|
||||
actions.push {
|
||||
name: 'delete'
|
||||
type: 'delete'
|
||||
icon: 'trash'
|
||||
href: '#'
|
||||
}
|
||||
for key in keys
|
||||
config = actionConfig[key]
|
||||
if config
|
||||
actions = config.action(actions, ticket, article, @)
|
||||
actions
|
||||
|
||||
facebookFeedReply: (e) =>
|
||||
actionPerform: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
type = App.TicketArticleType.findByAttribute('name', 'facebook feed comment')
|
||||
@scrollToCompose()
|
||||
articleContainer = $(e.target).closest('.ticket-article-item')
|
||||
type = $(e.currentTarget).attr('data-type')
|
||||
ticket = App.Ticket.fullLocal(@ticket.id)
|
||||
article = App.TicketArticle.fullLocal(@article.id)
|
||||
|
||||
# empty form
|
||||
articleNew = {
|
||||
to: ''
|
||||
cc: ''
|
||||
body: ''
|
||||
in_reply_to: ''
|
||||
}
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
|
||||
|
||||
twitterStatusReply: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
# get reference article
|
||||
article_id = $(e.target).parents('[data-id]').data('id')
|
||||
article = App.TicketArticle.fullLocal(article_id)
|
||||
sender = App.TicketArticleSender.find(article.sender_id)
|
||||
type = App.TicketArticleType.find(article.type_id)
|
||||
customer = App.User.find(article.created_by_id)
|
||||
|
||||
@scrollToCompose()
|
||||
|
||||
# empty form
|
||||
articleNew = {
|
||||
to: ''
|
||||
cc: ''
|
||||
body: ''
|
||||
in_reply_to: ''
|
||||
}
|
||||
|
||||
if article.message_id
|
||||
articleNew.in_reply_to = article.message_id
|
||||
|
||||
# get current body
|
||||
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
|
||||
articleNew.body = body
|
||||
|
||||
recipients = article.from
|
||||
if article.to
|
||||
if recipients
|
||||
recipients += ', '
|
||||
recipients += article.to
|
||||
|
||||
if recipients
|
||||
recipientString = ''
|
||||
recipientScreenNames = recipients.split(',')
|
||||
for recipientScreenName in recipientScreenNames
|
||||
if recipientScreenName
|
||||
recipientScreenName = recipientScreenName.trim().toLowerCase()
|
||||
|
||||
# exclude already listed screen name
|
||||
exclude = false
|
||||
if body && body.toLowerCase().match(recipientScreenName)
|
||||
exclude = true
|
||||
|
||||
# exclude own screen_name
|
||||
if recipientScreenName is "@#{@ticket.preferences.channel_screen_name}".toLowerCase()
|
||||
exclude = true
|
||||
|
||||
if exclude is false
|
||||
if recipientString isnt ''
|
||||
recipientString += ' '
|
||||
recipientString += recipientScreenName
|
||||
|
||||
if body
|
||||
articleNew.body = "#{recipientString} #{body} "
|
||||
else
|
||||
articleNew.body = "#{recipientString} "
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew, position: 'end' } )
|
||||
|
||||
twitterDirectMessageReply: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
# get reference article
|
||||
article_id = $(e.target).parents('[data-id]').data('id')
|
||||
article = App.TicketArticle.fullLocal(article_id)
|
||||
type = App.TicketArticleType.find(article.type_id)
|
||||
sender = App.TicketArticleSender.find(article.sender_id)
|
||||
customer = App.User.find(article.created_by_id)
|
||||
|
||||
@scrollToCompose()
|
||||
|
||||
# empty form
|
||||
articleNew = {
|
||||
to: ''
|
||||
cc: ''
|
||||
body: ''
|
||||
in_reply_to: ''
|
||||
}
|
||||
|
||||
if article.message_id
|
||||
articleNew.in_reply_to = article.message_id
|
||||
|
||||
if sender.name is 'Agent'
|
||||
articleNew.to = article.to
|
||||
else
|
||||
articleNew.to = article.from
|
||||
|
||||
if !articleNew.to
|
||||
articleNew.to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
|
||||
|
||||
emailReplyAll: (e) =>
|
||||
@emailReply(e, true)
|
||||
|
||||
emailReply: (e, all = false) =>
|
||||
e.preventDefault()
|
||||
|
||||
# get reference article
|
||||
article_id = $(e.target).parents('[data-id]').data('id')
|
||||
article = App.TicketArticle.fullLocal(article_id)
|
||||
ticket = App.Ticket.fullLocal(article.ticket_id)
|
||||
type = App.TicketArticleType.find(article.type_id)
|
||||
article_created_by = App.User.find(article.created_by_id)
|
||||
email_addresses = App.EmailAddress.all()
|
||||
|
||||
@scrollToCompose()
|
||||
|
||||
# empty form
|
||||
articleNew = App.Utils.getRecipientArticle(ticket, article, article_created_by, type, email_addresses, all)
|
||||
|
||||
# get current body
|
||||
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
|
||||
|
||||
# check if quote need to be added
|
||||
signaturePosition = 'bottom'
|
||||
selected = App.ClipBoard.getSelected('html')
|
||||
if selected
|
||||
selected = App.Utils.htmlCleanup(selected).html()
|
||||
if !selected
|
||||
selected = App.ClipBoard.getSelected('text')
|
||||
if selected
|
||||
selected = App.Utils.textCleanup(selected)
|
||||
selected = App.Utils.text2html(selected)
|
||||
|
||||
# full quote, if needed
|
||||
if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote')
|
||||
signaturePosition = 'top'
|
||||
if article.content_type.match('html')
|
||||
selected = App.Utils.textCleanup(article.body)
|
||||
if article.content_type.match('plain')
|
||||
selected = App.Utils.textCleanup(article.body)
|
||||
selected = App.Utils.text2html(selected)
|
||||
|
||||
if selected
|
||||
selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>"
|
||||
|
||||
# add selected text to body
|
||||
body = selected + body
|
||||
|
||||
articleNew.body = body
|
||||
|
||||
type = App.TicketArticleType.findByAttribute(name:'email')
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', {
|
||||
ticket: @ticket
|
||||
type: type
|
||||
article: articleNew
|
||||
signaturePosition: signaturePosition
|
||||
})
|
||||
|
||||
telegramPersonalMessageReply: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
# get reference article
|
||||
article_id = $(e.target).parents('[data-id]').data('id')
|
||||
article = App.TicketArticle.fullLocal(article_id)
|
||||
sender = App.TicketArticleSender.find(article.sender_id)
|
||||
type = App.TicketArticleType.find(article.type_id)
|
||||
customer = App.User.find(article.created_by_id)
|
||||
|
||||
@scrollToCompose()
|
||||
|
||||
# empty form
|
||||
articleNew = {
|
||||
to: ''
|
||||
cc: ''
|
||||
body: ''
|
||||
in_reply_to: ''
|
||||
}
|
||||
|
||||
if article.message_id
|
||||
articleNew.in_reply_to = article.message_id
|
||||
|
||||
# get current body
|
||||
articleNew.body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html().trim() || ''
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew, position: 'end' } )
|
||||
|
||||
delete: (e) =>
|
||||
e.preventDefault()
|
||||
|
||||
callback = ->
|
||||
article_id = $(e.target).parents('[data-id]').data('id')
|
||||
article = App.TicketArticle.find(article_id)
|
||||
article.destroy()
|
||||
|
||||
new App.ControllerConfirm(
|
||||
message: 'Sure?'
|
||||
callback: callback
|
||||
container: @el.closest('.content')
|
||||
)
|
||||
actionConfig = App.Config.get('TicketZoomArticleAction')
|
||||
keys = _.keys(actionConfig).sort()
|
||||
actions = []
|
||||
for key in keys
|
||||
config = actionConfig[key]
|
||||
if config
|
||||
return if !config.perform(articleContainer, type, ticket, article, @)
|
||||
|
||||
scrollToCompose: =>
|
||||
@el.closest('.content').find('.article-add').ScrollTo()
|
||||
|
|
|
@ -70,6 +70,14 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@textarea.focus()
|
||||
)
|
||||
|
||||
# add article attachment
|
||||
@bind('ui::ticket::addArticleAttachent', (data) =>
|
||||
return if data.ticket.id.toString() isnt @ticket_id.toString()
|
||||
return if _.isEmpty(data.attachments)
|
||||
for file in data.attachments
|
||||
@renderAttachment(file)
|
||||
)
|
||||
|
||||
# reset new article screen
|
||||
@bind('ui::ticket::taskReset', (data) =>
|
||||
return if data.ticket_id.toString() isnt @ticket_id.toString()
|
||||
|
@ -143,8 +151,8 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
icon: 'twitter'
|
||||
attributes: []
|
||||
internal: false,
|
||||
features: ['body:limit', 'body:initials']
|
||||
maxTextLength: 140
|
||||
features: attributes
|
||||
maxTextLength: 280
|
||||
warningTextLength: 30
|
||||
}
|
||||
if possibleArticleType['twitter direct-message']
|
||||
|
@ -156,7 +164,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
icon: 'twitter'
|
||||
attributes: ['to']
|
||||
internal: false,
|
||||
features: ['body:limit', 'body:initials']
|
||||
features: attributes
|
||||
maxTextLength: 10000
|
||||
warningTextLength: 500
|
||||
}
|
||||
|
@ -245,7 +253,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
controller = new App.ControllerForm(
|
||||
el: @$('.recipients')
|
||||
model:
|
||||
configure_attributes: configure_attributes,
|
||||
configure_attributes: configure_attributes
|
||||
)
|
||||
|
||||
@$('[data-name="body"]').ce({
|
||||
|
@ -255,12 +263,13 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
})
|
||||
|
||||
html5Upload.initialize(
|
||||
uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload',
|
||||
dropContainer: @$('.article-add').get(0),
|
||||
cancelContainer: @cancelContainer,
|
||||
inputField: @$('.article-attachment input').get(0),
|
||||
key: 'File',
|
||||
data: { form_id: @form_id },
|
||||
uploadUrl: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
dropContainer: @$('.article-add').get(0)
|
||||
cancelContainer: @cancelContainer
|
||||
inputField: @$('.article-attachment input').get(0)
|
||||
key: 'File'
|
||||
data:
|
||||
form_id: @form_id
|
||||
maxSimultaneousUploads: 1,
|
||||
onFileAdded: (file) =>
|
||||
|
||||
|
@ -303,6 +312,8 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
)
|
||||
)
|
||||
|
||||
@bindAttachmentDelete()
|
||||
|
||||
# show text module UI
|
||||
if !@permissionCheck('ticket.customer')
|
||||
textModule = new App.WidgetTextModule(
|
||||
|
@ -737,33 +748,29 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@articleNewEdit.parent().removeClass('is-dropTarget') if @dragEventCounter is 0
|
||||
|
||||
renderAttachment: (file) =>
|
||||
@attachmentsHolder.append App.view('generic/attachment_item')
|
||||
fileName: file.filename
|
||||
fileSize: @humanFileSize( file.size )
|
||||
store_id: file.store_id
|
||||
@attachmentsHolder.on(
|
||||
'click'
|
||||
"[data-id=#{file.store_id}]", (e) =>
|
||||
@attachments = _.filter(
|
||||
@attachments,
|
||||
(item) ->
|
||||
return if item.id isnt file.store_id
|
||||
item
|
||||
)
|
||||
store_id = $(e.currentTarget).data('id')
|
||||
@attachmentsHolder.append(App.view('generic/attachment_item')(file))
|
||||
|
||||
# delete attachment from storage
|
||||
App.Ajax.request(
|
||||
type: 'DELETE'
|
||||
url: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
data: JSON.stringify(store_id: store_id)
|
||||
processData: false
|
||||
)
|
||||
bindAttachmentDelete: =>
|
||||
@attachmentsHolder.on('click', '.js-delete', (e) =>
|
||||
id = $(e.currentTarget).data('id')
|
||||
@attachments = _.filter(
|
||||
@attachments,
|
||||
(item) ->
|
||||
return if item.id.toString() is id.toString()
|
||||
item
|
||||
)
|
||||
|
||||
# remove attachment from dom
|
||||
element = $(e.currentTarget).closest('.attachments')
|
||||
$(e.currentTarget).closest('.attachment').remove()
|
||||
# empty .attachment (remove spaces) to keep css working, thanks @mrflix :-o
|
||||
if element.find('.attachment').length == 0
|
||||
element.empty()
|
||||
# delete attachment from storage
|
||||
App.Ajax.request(
|
||||
type: 'DELETE'
|
||||
url: App.Config.get('api_path') + '/ticket_attachment_upload'
|
||||
data: JSON.stringify(id: id)
|
||||
processData: false
|
||||
)
|
||||
|
||||
# remove attachment from dom
|
||||
element = $(e.currentTarget).closest('.attachments')
|
||||
$(e.currentTarget).closest('.attachment').remove()
|
||||
if element.find('.attachment').length == 0
|
||||
element.empty()
|
||||
)
|
||||
|
|
|
@ -20,6 +20,7 @@ class App.TicketZoomArticleView extends App.Controller
|
|||
el: el
|
||||
ui: @ui
|
||||
highligher: @highligher
|
||||
form_id: @form_id
|
||||
)
|
||||
if !@ticketArticleInsertByIndex(index, el)
|
||||
all.push el
|
||||
|
@ -193,6 +194,7 @@ class ArticleViewItem extends App.ObserverController
|
|||
ticket: @ticket
|
||||
article: article
|
||||
lastAttributres: @lastAttributres
|
||||
form_id: @form_id
|
||||
)
|
||||
|
||||
# set see more
|
||||
|
|
|
@ -32,16 +32,14 @@ class App.TicketZoomSidebar extends App.ObserverController
|
|||
)
|
||||
else
|
||||
@sidebarBackends[key].reload(
|
||||
params: @params
|
||||
query: @query
|
||||
params: @params
|
||||
query: @query
|
||||
formMeta: @formMeta
|
||||
markForm: @markForm
|
||||
tags: @tags
|
||||
links: @links
|
||||
)
|
||||
item = @sidebarBackends[key].sidebarItem()
|
||||
if item
|
||||
@sidebarItems.push item
|
||||
@sidebarItems.push @sidebarBackends[key]
|
||||
|
||||
new App.Sidebar(
|
||||
el: @el.find('.tabsSidebar')
|
||||
|
|
|
@ -1,32 +1,67 @@
|
|||
class SidebarCustomer extends App.Controller
|
||||
sidebarItem: =>
|
||||
return if !@permissionCheck('ticket.agent')
|
||||
items = {
|
||||
head: 'Customer'
|
||||
name: 'customer'
|
||||
icon: 'person'
|
||||
actions: [
|
||||
@item = {
|
||||
name: 'customer'
|
||||
badgeCallback: @badgeRender
|
||||
sidebarHead: 'Customer'
|
||||
sidebarCallback: @showCustomer
|
||||
sidebarActions: [
|
||||
{
|
||||
title: 'Change Customer'
|
||||
name: 'customer-change'
|
||||
callback: @changeCustomer
|
||||
},
|
||||
]
|
||||
callback: @showCustomer
|
||||
}
|
||||
return items if @ticket && @ticket.customer_id == 1
|
||||
items.actions.push {
|
||||
return @item if @ticket && @ticket.customer_id == 1
|
||||
@item.sidebarActions.push {
|
||||
title: 'Edit Customer'
|
||||
name: 'customer-edit'
|
||||
callback: @editCustomer
|
||||
}
|
||||
items
|
||||
@item
|
||||
|
||||
metaBadge: (user) =>
|
||||
counter = ''
|
||||
cssClass = ''
|
||||
counter = @sidebarItemCounter(user)
|
||||
|
||||
if @Config.get('ui_sidebar_open_ticket_indicator_colored') is true
|
||||
if counter == 1
|
||||
cssClass = 'tabsSidebar-tab-count--warning'
|
||||
if counter > 1
|
||||
cssClass = 'tabsSidebar-tab-count--danger'
|
||||
|
||||
{
|
||||
name: 'customer'
|
||||
icon: 'person'
|
||||
counterPossible: true
|
||||
counter: counter
|
||||
cssClass: cssClass
|
||||
}
|
||||
|
||||
badgeRender: (el) =>
|
||||
@badgeEl = el
|
||||
if App.User.exists(@ticket.customer_id)
|
||||
user = App.User.find(@ticket.customer_id)
|
||||
@badgeRenderLocal(user)
|
||||
|
||||
badgeRenderLocal: (user) =>
|
||||
@badgeEl.html(App.view('generic/sidebar_tabs_item')(@metaBadge(user)))
|
||||
|
||||
sidebarItemCounter: (user) ->
|
||||
counter = ''
|
||||
if user && user.preferences && user.preferences.tickets_open
|
||||
counter = user.preferences.tickets_open
|
||||
counter
|
||||
|
||||
showCustomer: (el) =>
|
||||
@el = el
|
||||
@elSidebar = el
|
||||
new App.WidgetUser(
|
||||
el: @el
|
||||
el: @elSidebar
|
||||
user_id: @ticket.customer_id
|
||||
callback: @badgeRenderLocal
|
||||
)
|
||||
|
||||
editCustomer: =>
|
||||
|
@ -38,13 +73,13 @@ class SidebarCustomer extends App.Controller
|
|||
title: 'Users'
|
||||
object: 'User'
|
||||
objects: 'Users'
|
||||
container: @el.closest('.content')
|
||||
container: @elSidebar.closest('.content')
|
||||
)
|
||||
|
||||
changeCustomer: =>
|
||||
new App.TicketCustomer(
|
||||
ticket_id: @ticket.id
|
||||
container: @el.closest('.content')
|
||||
container: @elSidebar.closest('.content')
|
||||
)
|
||||
|
||||
App.Config.set('200-Customer', SidebarCustomer, 'TicketZoomSidebar')
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
class SidebarIdoit extends App.Controller
|
||||
sidebarItem: =>
|
||||
return if !@Config.get('idoit_integration')
|
||||
{
|
||||
head: 'i-doit'
|
||||
name: 'idoit'
|
||||
icon: 'printer'
|
||||
actions: [
|
||||
@item = {
|
||||
name: 'idoit'
|
||||
badgeIcon: 'printer'
|
||||
sidebarHead: 'i-doit'
|
||||
sidebarCallback: @showObjects
|
||||
sidebarActions: [
|
||||
{
|
||||
title: 'Change Objects'
|
||||
name: 'objects-change'
|
||||
callback: @changeObjects
|
||||
},
|
||||
]
|
||||
callback: @showObjects
|
||||
}
|
||||
@item
|
||||
|
||||
changeObjects: =>
|
||||
new App.IdoitObjectSelector(
|
||||
|
|
|
@ -2,24 +2,25 @@ class SidebarOrganization extends App.Controller
|
|||
sidebarItem: =>
|
||||
return if !@permissionCheck('ticket.agent')
|
||||
return if !@ticket.organization_id
|
||||
{
|
||||
head: 'Organization'
|
||||
@item = {
|
||||
name: 'organization'
|
||||
icon: 'group'
|
||||
actions: [
|
||||
badgeIcon: 'group'
|
||||
sidebarHead: 'Organization'
|
||||
sidebarCallback: @showOrganization
|
||||
sidebarActions: [
|
||||
{
|
||||
title: 'Edit Organization'
|
||||
name: 'organization-edit'
|
||||
callback: @editOrganization
|
||||
},
|
||||
]
|
||||
callback: @showOrganization
|
||||
}
|
||||
@item
|
||||
|
||||
showOrganization: (el) =>
|
||||
@el = el
|
||||
@elSidebar = el
|
||||
new App.WidgetOrganization(
|
||||
el: @el
|
||||
el: @elSidebar
|
||||
organization_id: @ticket.organization_id
|
||||
)
|
||||
|
||||
|
@ -31,7 +32,7 @@ class SidebarOrganization extends App.Controller
|
|||
title: 'Organizations'
|
||||
object: 'Organization'
|
||||
objects: 'Organizations'
|
||||
container: @el.closest('.content')
|
||||
container: @elSidebar.closest('.content')
|
||||
)
|
||||
|
||||
App.Config.set('300-Organization', SidebarOrganization, 'TicketZoomSidebar')
|
||||
|
|
|
@ -20,8 +20,9 @@ class Edit extends App.ObserverController
|
|||
handlers: [
|
||||
@ticketFormChanges
|
||||
]
|
||||
filter: @formMeta.filter
|
||||
params: defaults
|
||||
filter: @formMeta.filter
|
||||
params: defaults
|
||||
isDisabled: !ticket.editable()
|
||||
#bookmarkable: true
|
||||
)
|
||||
|
||||
|
@ -36,14 +37,14 @@ class Edit extends App.ObserverController
|
|||
|
||||
class SidebarTicket extends App.Controller
|
||||
sidebarItem: =>
|
||||
sidebarItem = {
|
||||
head: 'Ticket'
|
||||
name: 'ticket'
|
||||
icon: 'message'
|
||||
callback: @editTicket
|
||||
@item = {
|
||||
name: 'ticket'
|
||||
badgeIcon: 'message'
|
||||
sidebarHead: 'Ticket'
|
||||
sidebarCallback: @editTicket
|
||||
}
|
||||
if @permissionCheck('ticket.agent')
|
||||
sidebarItem['actions'] = [
|
||||
@item.sidebarActions = [
|
||||
{
|
||||
title: 'History'
|
||||
name: 'ticket-history'
|
||||
|
@ -60,7 +61,7 @@ class SidebarTicket extends App.Controller
|
|||
callback: @changeCustomer
|
||||
},
|
||||
]
|
||||
sidebarItem
|
||||
@item
|
||||
|
||||
reload: (args) =>
|
||||
|
||||
|
@ -79,7 +80,7 @@ class SidebarTicket extends App.Controller
|
|||
|
||||
editTicket: (el) =>
|
||||
@el = el
|
||||
localEl = $( App.view('ticket_zoom/sidebar_ticket')() )
|
||||
localEl = $(App.view('ticket_zoom/sidebar_ticket')())
|
||||
|
||||
@edit = new Edit(
|
||||
object_id: @ticket.id
|
||||
|
|
|
@ -145,7 +145,7 @@ class Index extends App.ControllerSubContent
|
|||
query: @query
|
||||
limit: 140
|
||||
role_ids: role_ids
|
||||
full: 1
|
||||
full: true
|
||||
processData: true,
|
||||
success: (data, status, xhr) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
|
@ -167,7 +167,7 @@ class Index extends App.ControllerSubContent
|
|||
data:
|
||||
limit: 50
|
||||
role_ids: role_ids
|
||||
full: 1
|
||||
full: true
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
App.Collection.loadAssets(data.assets)
|
||||
|
|
|
@ -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')
|
|
@ -47,4 +47,4 @@ class Widget extends App.ControllerWidgetOnDemand
|
|||
800
|
||||
)
|
||||
|
||||
App.Config.set( 'switch_back_to_user', Widget, 'Widgets' )
|
||||
App.Config.set('switch_back_to_user', Widget, 'Widgets')
|
||||
|
|
|
@ -70,6 +70,7 @@ class App.TicketStats extends App.Controller
|
|||
render: (data) =>
|
||||
if !data
|
||||
data = @data
|
||||
return if !data
|
||||
|
||||
user_total = 0
|
||||
if data.user.open_ids && data.user.closed_ids
|
||||
|
|
|
@ -12,8 +12,8 @@ class App extends Spine.Controller
|
|||
helper =
|
||||
|
||||
# define print name helper
|
||||
P: (object, attributeName, attributes) ->
|
||||
App.viewPrint(object, attributeName, attributes)
|
||||
P: (object, attributeName, attributes, table = false) ->
|
||||
App.viewPrint(object, attributeName, attributes, table)
|
||||
|
||||
# define date format helper
|
||||
date: (time) ->
|
||||
|
@ -136,7 +136,7 @@ class App extends Spine.Controller
|
|||
return marked(string)
|
||||
App.i18n.translateContent(string)
|
||||
|
||||
@viewPrint: (object, attributeName, attributes) ->
|
||||
@viewPrint: (object, attributeName, attributes, table) ->
|
||||
if !attributes
|
||||
attributes = {}
|
||||
if object.constructor.attributesGet
|
||||
|
@ -172,10 +172,10 @@ class App extends Spine.Controller
|
|||
if object[attributeNameWithoutRef]
|
||||
valueRef = object[attributeNameWithoutRef]
|
||||
|
||||
@viewPrintItem(value, attributeConfig, valueRef)
|
||||
@viewPrintItem(value, attributeConfig, valueRef, table)
|
||||
|
||||
# define print name helper
|
||||
@viewPrintItem: (item, attributeConfig = {}, valueRef) ->
|
||||
@viewPrintItem: (item, attributeConfig = {}, valueRef, table) ->
|
||||
return '-' if item is undefined
|
||||
return '-' if item is ''
|
||||
return item if item is null
|
||||
|
@ -238,7 +238,7 @@ class App extends Spine.Controller
|
|||
# transform date
|
||||
if attributeConfig.tag is 'date'
|
||||
isHtmlEscape = true
|
||||
resultLocal = App.i18n.translateDate(resultLocal)
|
||||
resultLocal = App.i18n.translateDate(resultLocal)
|
||||
|
||||
# transform input tel|url to make it clickable
|
||||
if attributeConfig.tag is 'input'
|
||||
|
@ -258,8 +258,10 @@ class App extends Spine.Controller
|
|||
cssClass = attributeConfig.class || ''
|
||||
if cssClass.match 'escalation'
|
||||
escalation = true
|
||||
humanTime = App.PrettyDate.humanTime(resultLocal, escalation)
|
||||
resultLocal = "<time class=\"humanTimeFromNow #{cssClass}\" data-time=\"#{resultLocal}\" title=\"#{timestamp}\">#{humanTime}</time>"
|
||||
humanTime = ''
|
||||
if !table
|
||||
humanTime = App.PrettyDate.humanTime(resultLocal, escalation)
|
||||
resultLocal = "<time class=\"humanTimeFromNow #{cssClass}\" data-time=\"#{resultLocal}\" title=\"#{timestamp}\">#{humanTime}</time>"
|
||||
|
||||
if !isHtmlEscape && typeof resultLocal is 'string'
|
||||
resultLocal = App.Utils.htmlEscape(resultLocal)
|
||||
|
|
|
@ -77,7 +77,7 @@ class App._CollectionSingletonBase
|
|||
|
||||
callback: (data) =>
|
||||
for counter, attr of @callbacks
|
||||
callback = ->
|
||||
callback = =>
|
||||
attr.callback(data)
|
||||
if attr.one
|
||||
delete @callbacks[counter]
|
||||
|
|
|
@ -71,7 +71,7 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
@open()
|
||||
|
||||
focusInput: =>
|
||||
@objectSelect.focus() if not @formControl.hasClass 'focus'
|
||||
@objectSelect.focus() if not @formControl.hasClass('focus')
|
||||
|
||||
onBlur: =>
|
||||
selectObject = @objectSelect.val()
|
||||
|
@ -85,6 +85,9 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
@objectId.val("guess:#{selectObject}")
|
||||
@formControl.removeClass 'focus'
|
||||
|
||||
resetObjectSelection: =>
|
||||
@objectId.val('').trigger('change')
|
||||
|
||||
onObjectClick: (e) =>
|
||||
objectId = $(e.currentTarget).data('object-id')
|
||||
@selectObject(objectId)
|
||||
|
@ -103,23 +106,23 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
# Only work with the last one since its the newest one
|
||||
objectId = @objectId.val().split(',').pop()
|
||||
|
||||
return if !objectId
|
||||
return if !App[@objectSingle].exists(objectId)
|
||||
object = App[@objectSingle].find(objectId)
|
||||
name = object.displayName()
|
||||
if objectId && App[@objectSingle].exists(objectId)
|
||||
object = App[@objectSingle].find(objectId)
|
||||
name = object.displayName()
|
||||
|
||||
if @attribute.multiple
|
||||
# create token
|
||||
@createToken name, objectId
|
||||
else
|
||||
if object.email
|
||||
if @attribute.multiple
|
||||
|
||||
# quote name for special character
|
||||
if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/)
|
||||
name = "\"#{name}\""
|
||||
name += " <#{object.email}>"
|
||||
# create token
|
||||
@createToken(name, objectId)
|
||||
else
|
||||
if object.email
|
||||
|
||||
@objectSelect.val(name)
|
||||
# quote name for special character
|
||||
if name.match(/\@|,|;|\^|\+|#|§|\$|%|&|\/|\(|\)|=|\?|!|\*|\[|\]/)
|
||||
name = "\"#{name}\""
|
||||
name += " <#{object.email}>"
|
||||
|
||||
@objectSelect.val(name)
|
||||
|
||||
if @callback
|
||||
@callback(objectId)
|
||||
|
@ -321,12 +324,16 @@ class App.ObjectOrganizationAutocompletion extends App.Controller
|
|||
@hideOrganizationMembers()
|
||||
|
||||
# hide dropdown
|
||||
if !query
|
||||
if _.isEmpty(query)
|
||||
@emptyResultList()
|
||||
|
||||
if !@attribute.disableCreateObject
|
||||
@recipientList.append(@buildObjectNew())
|
||||
|
||||
# reset object selection
|
||||
@resetObjectSelection()
|
||||
return
|
||||
|
||||
# show dropdown
|
||||
if query && ( !@attribute.minLengt || @attribute.minLengt <= query.length )
|
||||
@lazySearch(query)
|
||||
|
|
|
@ -79,8 +79,7 @@ class App.Auth
|
|||
@_updateModelAttributes(data.models)
|
||||
|
||||
# set locale
|
||||
locale = window.navigator.userLanguage || window.navigator.language || 'en-us'
|
||||
App.i18n.set(locale)
|
||||
App.i18n.set(App.i18n.detectBrowserLocale())
|
||||
|
||||
# rebuild navbar with new navbar items
|
||||
App.Event.trigger('auth')
|
||||
|
@ -120,7 +119,7 @@ class App.Auth
|
|||
if preferences && preferences.locale
|
||||
locale = preferences.locale
|
||||
if !locale
|
||||
locale = window.navigator.userLanguage || window.navigator.language || 'en-us'
|
||||
locale = App.i18n.detectBrowserLocale()
|
||||
App.i18n.set(locale)
|
||||
|
||||
App.Event.trigger('auth:login', data.session)
|
||||
|
|
|
@ -39,14 +39,27 @@ class App.ColumnSelect extends Spine.Controller
|
|||
)
|
||||
|
||||
render: ->
|
||||
if !_.isEmpty(@attribute.seperator)
|
||||
values = []
|
||||
if @attribute.value
|
||||
values = @attribute.value.split(';')
|
||||
else if @attribute.default
|
||||
values = @attribute.default.split(';')
|
||||
|
||||
for value in values
|
||||
for option in @options.attribute.options
|
||||
if option.value is value
|
||||
option.selected = true
|
||||
|
||||
@values = []
|
||||
_.each @options.attribute.options, (option) =>
|
||||
if option.selected
|
||||
@values.push option.value.toString()
|
||||
|
||||
@html App.view('generic/column_select')
|
||||
@html App.view('generic/column_select')(
|
||||
attribute: @options.attribute
|
||||
values: @values
|
||||
)
|
||||
|
||||
# keep inital height
|
||||
# disabled for now since controls in modals get rendered hidden
|
||||
|
@ -60,13 +73,17 @@ class App.ColumnSelect extends Spine.Controller
|
|||
@throttledSelect()
|
||||
|
||||
select: (value) ->
|
||||
@selected.find("[data-value='#{value}']").removeClass 'is-hidden'
|
||||
@pool.find("[data-value='#{value}']").addClass 'is-hidden'
|
||||
@selected.find("[data-value='#{value}']").removeClass('is-hidden')
|
||||
@pool.find("[data-value='#{value}']").addClass('is-hidden')
|
||||
@values.push(value)
|
||||
@shadow.val(@values)
|
||||
@shadow.trigger('change')
|
||||
|
||||
@placeholder.addClass 'is-hidden'
|
||||
if !_.isEmpty(@attribute.seperator)
|
||||
@shadow.val(@values.join(';'))
|
||||
else
|
||||
@shadow.val(@values)
|
||||
@shadow.trigger('change')
|
||||
|
||||
@placeholder.addClass('is-hidden')
|
||||
|
||||
if @search.val() and @poolOptions.not('.is-filtered').not('.is-hidden').size() is 0
|
||||
@clear()
|
||||
|
@ -76,14 +93,17 @@ class App.ColumnSelect extends Spine.Controller
|
|||
@throttledRemove()
|
||||
|
||||
remove: (value) ->
|
||||
@pool.find("[data-value='#{value}']").removeClass 'is-hidden'
|
||||
@selected.find("[data-value='#{value}']").addClass 'is-hidden'
|
||||
@pool.find("[data-value='#{value}']").removeClass('is-hidden')
|
||||
@selected.find("[data-value='#{value}']").addClass('is-hidden')
|
||||
@values.splice(@values.indexOf(value), 1)
|
||||
@shadow.val(@values)
|
||||
@shadow.trigger('change')
|
||||
if !_.isEmpty(@attribute.seperator)
|
||||
@shadow.val(@values.join(';'))
|
||||
else
|
||||
@shadow.val(@values)
|
||||
@shadow.trigger('change')
|
||||
|
||||
if !@values.length
|
||||
@placeholder.removeClass 'is-hidden'
|
||||
@placeholder.removeClass('is-hidden')
|
||||
|
||||
filter: (event) ->
|
||||
filter = $(event.currentTarget).val()
|
||||
|
@ -92,16 +112,16 @@ class App.ColumnSelect extends Spine.Controller
|
|||
return if $(el).hasClass('is-hidden')
|
||||
|
||||
if $(el).text().toLowerCase().indexOf(filter.toLowerCase()) > -1
|
||||
$(el).removeClass 'is-filtered'
|
||||
$(el).removeClass('is-filtered')
|
||||
else
|
||||
$(el).addClass 'is-filtered'
|
||||
$(el).addClass('is-filtered')
|
||||
|
||||
@clearButton.toggleClass 'is-hidden', filter.length is 0
|
||||
|
||||
clear: ->
|
||||
@search.val('')
|
||||
@poolOptions.removeClass 'is-filtered'
|
||||
@clearButton.addClass 'is-hidden'
|
||||
@poolOptions.removeClass('is-filtered')
|
||||
@clearButton.addClass('is-hidden')
|
||||
|
||||
onFilterKeydown: (event) ->
|
||||
return if event.keyCode != 13
|
||||
|
@ -111,4 +131,4 @@ class App.ColumnSelect extends Spine.Controller
|
|||
|
||||
firstVisibleOption = @poolOptions.not('.is-filtered').not('.is-hidden').first()
|
||||
if firstVisibleOption
|
||||
@select firstVisibleOption.attr('data-value')
|
||||
@select firstVisibleOption.attr('data-value')
|
||||
|
|
|
@ -80,6 +80,24 @@ class App.i18n
|
|||
_instance ?= new _i18nSingleton()
|
||||
_instance.mapTime
|
||||
|
||||
@detectBrowserLocale: ->
|
||||
return 'en-us' if !window.navigator.userLanguage && !window.navigator.language
|
||||
|
||||
if window.navigator.languages
|
||||
allLocales = App.Locale.all()
|
||||
for browserLocale in window.navigator.languages
|
||||
for localAllLocale in allLocales
|
||||
if browserLocale is localAllLocale.locale
|
||||
return localAllLocale.locale
|
||||
|
||||
for browserLocale in window.navigator.languages
|
||||
browserLocale = browserLocale.substr(0, 2)
|
||||
for localAllLocale in allLocales
|
||||
if browserLocale is localAllLocale.alias
|
||||
return localAllLocale.locale
|
||||
|
||||
window.navigator.userLanguage || window.navigator.language || 'en-us'
|
||||
|
||||
class _i18nSingleton extends Spine.Module
|
||||
@include App.LogInclude
|
||||
|
||||
|
@ -319,18 +337,20 @@ class _i18nSingleton extends Spine.Module
|
|||
if offset
|
||||
timeObject = new Date(timeObject.getTime() + (timeObject.getTimezoneOffset() * 60000))
|
||||
|
||||
d = timeObject.getDate()
|
||||
m = timeObject.getMonth() + 1
|
||||
y = timeObject.getFullYear()
|
||||
S = timeObject.getSeconds()
|
||||
M = timeObject.getMinutes()
|
||||
H = timeObject.getHours()
|
||||
d = timeObject.getDate()
|
||||
m = timeObject.getMonth() + 1
|
||||
yfull = timeObject.getFullYear()
|
||||
yshort = timeObject.getYear()-100
|
||||
S = timeObject.getSeconds()
|
||||
M = timeObject.getMinutes()
|
||||
H = timeObject.getHours()
|
||||
format = format
|
||||
.replace(/dd/, s(d, 2))
|
||||
.replace(/d/, d)
|
||||
.replace(/mm/, s(m, 2))
|
||||
.replace(/m/, m)
|
||||
.replace(/yyyy/, y)
|
||||
.replace(/yyyy/, yfull)
|
||||
.replace(/yy/, yshort)
|
||||
.replace(/SS/, s(S, 2))
|
||||
.replace(/MM/, s(M, 2))
|
||||
.replace(/HH/, s(H, 2))
|
||||
|
|
|
@ -20,37 +20,53 @@ class App.ImageService
|
|||
imageWidth = imageObject.width
|
||||
imageHeight = imageObject.height
|
||||
console.log('ImageService', 'current size', imageWidth, imageHeight)
|
||||
console.log('ImageService', 'sizeFactor', sizeFactor)
|
||||
if y is 'auto' && x is 'auto'
|
||||
x = imageWidth
|
||||
y = imageHeight
|
||||
|
||||
# set max x/y
|
||||
if x isnt 'auto' && x > imageWidth
|
||||
x = imageWidth
|
||||
|
||||
if y isnt 'auto' && y > imageHeight
|
||||
y = imageHeight
|
||||
|
||||
# get auto dimensions
|
||||
if y is 'auto'
|
||||
if y is 'auto'# && (y * factor) >= imageHeight
|
||||
factor = imageWidth / x
|
||||
y = imageHeight / factor
|
||||
|
||||
if x is 'auto'
|
||||
if x is 'auto'# && (y * factor) >= imageWidth
|
||||
factor = imageWidth / y
|
||||
x = imageHeight / factor
|
||||
|
||||
canvas = document.createElement('canvas')
|
||||
|
||||
# check if resize is needed
|
||||
resize = false
|
||||
if x < imageWidth || y < imageHeight
|
||||
if (x < imageWidth && (x * sizeFactor < imageWidth)) || (y < imageHeight && (y * sizeFactor < imageHeight))
|
||||
resize = true
|
||||
x = x * sizeFactor
|
||||
y = y * sizeFactor
|
||||
|
||||
# set dimensions
|
||||
canvas.width = x
|
||||
canvas.height = y
|
||||
|
||||
# draw image on canvas and set image dimensions
|
||||
context = canvas.getContext('2d')
|
||||
context.drawImage(imageObject, 0, 0, x, y)
|
||||
|
||||
else
|
||||
x = imageWidth
|
||||
y = imageHeight
|
||||
|
||||
# create canvas and set dimensions
|
||||
canvas = document.createElement('canvas')
|
||||
canvas.width = x
|
||||
canvas.height = y
|
||||
# set dimensions
|
||||
canvas.width = imageWidth
|
||||
canvas.height = imageHeight
|
||||
|
||||
# draw image on canvas and set image dimensions
|
||||
context = canvas.getContext('2d')
|
||||
context.drawImage(imageObject, 0, 0, x, y)
|
||||
# draw image on canvas and set image dimensions
|
||||
context = canvas.getContext('2d')
|
||||
context.drawImage(imageObject, 0, 0, imageWidth, imageHeight)
|
||||
|
||||
# set quallity based on image size
|
||||
if quallity == 'auto'
|
||||
|
|
|
@ -5,6 +5,8 @@ class App.Utils
|
|||
'TD': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'valign', 'width', 'style']
|
||||
'TH': ['abbr', 'align', 'axis', 'colspan', 'headers', 'rowspan', 'scope', 'sorted', 'valign', 'width', 'style']
|
||||
'TR': ['width', 'style']
|
||||
'A': ['href', 'hreflang', 'name', 'rel']
|
||||
'IMG': ['align', 'alt', 'border', 'height', 'src', 'srcset', 'width', 'style']
|
||||
|
||||
@mapCss:
|
||||
'TABLE': [
|
||||
|
@ -14,15 +16,9 @@ class App.Utils
|
|||
'text-align',
|
||||
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
|
||||
|
||||
'border-top-width',
|
||||
'border-right-width',
|
||||
'border-bottom-width',
|
||||
'border-left-width',
|
||||
|
||||
'border-top-color',
|
||||
'border-right-color',
|
||||
'border-bottom-color',
|
||||
'border-left-color',
|
||||
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
||||
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
||||
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
|
||||
]
|
||||
'TH': [
|
||||
'background', 'background-color', 'color', 'font-size', 'vertical-align',
|
||||
|
@ -31,15 +27,10 @@ class App.Utils
|
|||
'text-align',
|
||||
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
|
||||
|
||||
'border-top-width',
|
||||
'border-right-width',
|
||||
'border-bottom-width',
|
||||
'border-left-width',
|
||||
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
||||
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
||||
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
|
||||
|
||||
'border-top-color',
|
||||
'border-right-color',
|
||||
'border-bottom-color',
|
||||
'border-left-color',
|
||||
]
|
||||
'TR': [
|
||||
'background', 'background-color', 'color', 'font-size', 'vertical-align',
|
||||
|
@ -48,15 +39,10 @@ class App.Utils
|
|||
'text-align',
|
||||
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
|
||||
|
||||
'border-top-width',
|
||||
'border-right-width',
|
||||
'border-bottom-width',
|
||||
'border-left-width',
|
||||
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
||||
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
||||
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
|
||||
|
||||
'border-top-color',
|
||||
'border-right-color',
|
||||
'border-bottom-color',
|
||||
'border-left-color',
|
||||
]
|
||||
'TD': [
|
||||
'background', 'background-color', 'color', 'font-size', 'vertical-align',
|
||||
|
@ -65,15 +51,13 @@ class App.Utils
|
|||
'text-align',
|
||||
'border', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-collapse', 'border-style', 'border-spacing',
|
||||
|
||||
'border-top-width',
|
||||
'border-right-width',
|
||||
'border-bottom-width',
|
||||
'border-left-width',
|
||||
'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width',
|
||||
'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color',
|
||||
'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style',
|
||||
|
||||
'border-top-color',
|
||||
'border-right-color',
|
||||
'border-bottom-color',
|
||||
'border-left-color',
|
||||
]
|
||||
'IMG': [
|
||||
'width', 'height',
|
||||
]
|
||||
|
||||
# textCleand = App.Utils.textCleanup(rawText)
|
||||
|
@ -230,7 +214,7 @@ class App.Utils
|
|||
# remove comments
|
||||
@_removeComments(html)
|
||||
|
||||
# remove work markup
|
||||
# remove word markup
|
||||
@_removeWordMarkup(html)
|
||||
|
||||
# remove tags, keep content
|
||||
|
@ -251,7 +235,7 @@ class App.Utils
|
|||
# remove comments
|
||||
@_removeComments(html)
|
||||
|
||||
# remove work markup
|
||||
# remove word markup
|
||||
@_removeWordMarkup(html)
|
||||
|
||||
# remove tags, keep content
|
||||
|
@ -275,11 +259,11 @@ class App.Utils
|
|||
# remove comments
|
||||
@_removeComments(html)
|
||||
|
||||
# remove work markup
|
||||
# remove word markup
|
||||
@_removeWordMarkup(html)
|
||||
|
||||
# remove tags, keep content
|
||||
html.find('a, font, small, time, form, label').replaceWith( ->
|
||||
html.find('font, small, time, form, label').replaceWith( ->
|
||||
$(@).contents()
|
||||
)
|
||||
|
||||
|
@ -303,7 +287,7 @@ class App.Utils
|
|||
)
|
||||
|
||||
# remove tags & content
|
||||
html.find('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove()
|
||||
html.find('font, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove()
|
||||
|
||||
# remove style and class
|
||||
@_cleanAttributes(html)
|
||||
|
@ -906,6 +890,29 @@ class App.Utils
|
|||
text = text.replace(/http(s|):\/\/[-A-Za-z0-9+&@#\/%?=~_\|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/img, placeholder)
|
||||
text.length
|
||||
|
||||
@parseAddressListLocal: (line) ->
|
||||
recipients = emailAddresses.parseAddressList(line)
|
||||
result = []
|
||||
if !_.isEmpty(recipients)
|
||||
for recipient in recipients
|
||||
if recipient && recipient.address
|
||||
result.push recipient.address
|
||||
return result
|
||||
|
||||
# workaround for email-addresses.js issue with this kind of
|
||||
# mail headers "From: invalid sender, realname <sender@example.com>"
|
||||
# email-addresses.js is returning null because it can't parse the
|
||||
# whole header
|
||||
if _.isEmpty(recipients) && line.match('@')
|
||||
recipients = line.split(',')
|
||||
re = /(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/
|
||||
for recipient in recipients
|
||||
if recipient && recipient.match('@')
|
||||
localResult = recipient.match(re)
|
||||
if localResult && localResult[0]
|
||||
result.push localResult[0]
|
||||
result
|
||||
|
||||
@getRecipientArticle: (ticket, article, article_created_by, type, email_addresses = [], all) ->
|
||||
|
||||
# empty form
|
||||
|
@ -954,16 +961,18 @@ class App.Utils
|
|||
# check if article sender is local
|
||||
senderIsLocal = false
|
||||
if !_.isEmpty(article.from)
|
||||
senders = emailAddresses.parseAddressList(article.from)
|
||||
if senders && senders[0] && senders[0].address
|
||||
senderIsLocal = isLocalAddress(senders[0].address)
|
||||
senders = App.Utils.parseAddressListLocal(article.from)
|
||||
if senders
|
||||
for sender in senders
|
||||
if sender && sender.match('@')
|
||||
senderIsLocal = isLocalAddress(sender)
|
||||
|
||||
# check if article recipient is local
|
||||
recipientIsLocal = false
|
||||
if !_.isEmpty(article.to)
|
||||
recipients = emailAddresses.parseAddressList(article.to)
|
||||
if recipients && recipients[0] && recipients[0].address
|
||||
recipientIsLocal = isLocalAddress(recipients[0].address)
|
||||
recipients = App.Utils.parseAddressListLocal(article.to)
|
||||
if recipients && recipients[0]
|
||||
recipientIsLocal = isLocalAddress(recipients[0])
|
||||
|
||||
# sender is local
|
||||
if senderIsLocal
|
||||
|
@ -987,14 +996,14 @@ class App.Utils
|
|||
|
||||
# filter for uniq recipients
|
||||
recipientAddresses = {}
|
||||
|
||||
addAddresses = (addressLine, line) ->
|
||||
lineNew = ''
|
||||
recipients = emailAddresses.parseAddressList(addressLine)
|
||||
recipients = App.Utils.parseAddressListLocal(addressLine)
|
||||
|
||||
if !_.isEmpty(recipients)
|
||||
for recipient in recipients
|
||||
if !_.isEmpty(recipient.address)
|
||||
localRecipientAddress = recipient.address.toString().toLowerCase()
|
||||
if !_.isEmpty(recipient)
|
||||
localRecipientAddress = recipient.toString().toLowerCase()
|
||||
|
||||
# check if address is not local
|
||||
if !isLocalAddress(localRecipientAddress)
|
||||
|
|
|
@ -161,7 +161,14 @@ window.linkify = (function(){
|
|||
}
|
||||
|
||||
// Push massaged link onto the array
|
||||
parts.push([ link, href ]);
|
||||
// 2018-10-30: me only link urls, not mailto link
|
||||
//parts.push([ link, href ]);
|
||||
if ( href && href.substr && href.substr(0,7) != 'mailto:') {
|
||||
parts.push([ link, href ]);
|
||||
}
|
||||
else {
|
||||
parts.push([ link, undefined ]);
|
||||
}
|
||||
};
|
||||
|
||||
// Push remaining non-link text onto the array.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
// email-addresses.js - RFC 5322 email address parser
|
||||
// v 2.0.1
|
||||
// v 3.0.1
|
||||
//
|
||||
// http://tools.ietf.org/html/rfc5322
|
||||
//
|
||||
|
@ -186,27 +186,7 @@ function parse5322(opts) {
|
|||
// "First Last" -> "First Last"
|
||||
// "First Last" -> "First Last"
|
||||
function collapseWhitespace(s) {
|
||||
function isWhitespace(c) {
|
||||
return c === ' ' ||
|
||||
c === '\t' ||
|
||||
c === '\r' ||
|
||||
c === '\n';
|
||||
}
|
||||
var i, str;
|
||||
str = "";
|
||||
for (i = 0; i < s.length; i += 1) {
|
||||
if (!isWhitespace(s[i]) || !isWhitespace(s[i + 1])) {
|
||||
str += s[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (isWhitespace(str[0])) {
|
||||
str = str.substring(1);
|
||||
}
|
||||
if (isWhitespace(str[str.length - 1])) {
|
||||
str = str.substring(0, str.length - 1);
|
||||
}
|
||||
return str;
|
||||
return s.replace(/([ \t]|\r\n)+/g, ' ').replace(/^\s*/, '').replace(/\s*$/, '');
|
||||
}
|
||||
|
||||
// UTF-8 pseudo-production (RFC 6532)
|
||||
|
@ -597,10 +577,14 @@ function parse5322(opts) {
|
|||
return wrap('domain', function domainCheckTLD() {
|
||||
var result = or(obsDomain, dotAtom, domainLiteral)();
|
||||
if (opts.rejectTLD) {
|
||||
if (result.semantic.indexOf('.') < 0) {
|
||||
if (result && result.semantic && result.semantic.indexOf('.') < 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// strip all whitespace from domains
|
||||
if (result) {
|
||||
result.semantic = result.semantic.replace(/\s+/g, '');
|
||||
}
|
||||
return result;
|
||||
}());
|
||||
}
|
||||
|
@ -612,6 +596,36 @@ function parse5322(opts) {
|
|||
)());
|
||||
}
|
||||
|
||||
// 3.6.2 Originator Fields
|
||||
// Below we only parse the field body, not the name of the field
|
||||
// like "From:", "Sender:", or "Reply-To:". Other libraries that
|
||||
// parse email headers can parse those and defer to these productions
|
||||
// for the "RFC 5322" part.
|
||||
|
||||
// RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields
|
||||
// from = "From:" (mailbox-list / address-list) CRLF
|
||||
function fromSpec() {
|
||||
return wrap('from', or(
|
||||
mailboxList,
|
||||
addressList
|
||||
)());
|
||||
}
|
||||
|
||||
// RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields
|
||||
// sender = "Sender:" (mailbox / address) CRLF
|
||||
function senderSpec() {
|
||||
return wrap('sender', or(
|
||||
mailbox,
|
||||
address
|
||||
)());
|
||||
}
|
||||
|
||||
// RFC 6854 2.1. Replacement of RFC 5322, Section 3.6.2. Originator Fields
|
||||
// reply-to = "Reply-To:" address-list CRLF
|
||||
function replyToSpec() {
|
||||
return wrap('reply-to', addressList());
|
||||
}
|
||||
|
||||
// 4.1. Miscellaneous Obsolete Tokens
|
||||
|
||||
// obs-NO-WS-CTL = %d1-8 / ; US-ASCII control
|
||||
|
@ -766,92 +780,186 @@ function parse5322(opts) {
|
|||
// ast analysis
|
||||
|
||||
function findNode(name, root) {
|
||||
var i, queue, node;
|
||||
var i, stack, node;
|
||||
if (root === null || root === undefined) { return null; }
|
||||
queue = [root];
|
||||
while (queue.length > 0) {
|
||||
node = queue.shift();
|
||||
stack = [root];
|
||||
while (stack.length > 0) {
|
||||
node = stack.pop();
|
||||
if (node.name === name) {
|
||||
return node;
|
||||
}
|
||||
for (i = 0; i < node.children.length; i += 1) {
|
||||
queue.push(node.children[i]);
|
||||
for (i = node.children.length - 1; i >= 0; i -= 1) {
|
||||
stack.push(node.children[i]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findAllNodes(name, root) {
|
||||
var i, queue, node, result;
|
||||
var i, stack, node, result;
|
||||
if (root === null || root === undefined) { return null; }
|
||||
queue = [root];
|
||||
stack = [root];
|
||||
result = [];
|
||||
while (queue.length > 0) {
|
||||
node = queue.shift();
|
||||
while (stack.length > 0) {
|
||||
node = stack.pop();
|
||||
if (node.name === name) {
|
||||
result.push(node);
|
||||
}
|
||||
for (i = 0; i < node.children.length; i += 1) {
|
||||
queue.push(node.children[i]);
|
||||
for (i = node.children.length - 1; i >= 0; i -= 1) {
|
||||
stack.push(node.children[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function findAllNodesNoChildren(names, root) {
|
||||
var i, stack, node, result, namesLookup;
|
||||
if (root === null || root === undefined) { return null; }
|
||||
stack = [root];
|
||||
result = [];
|
||||
namesLookup = {};
|
||||
for (i = 0; i < names.length; i += 1) {
|
||||
namesLookup[names[i]] = true;
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
node = stack.pop();
|
||||
if (node.name in namesLookup) {
|
||||
result.push(node);
|
||||
// don't look at children (hence findAllNodesNoChildren)
|
||||
} else {
|
||||
for (i = node.children.length - 1; i >= 0; i -= 1) {
|
||||
stack.push(node.children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function giveResult(ast) {
|
||||
function grabSemantic(n) {
|
||||
return n !== null ? n.semantic : null;
|
||||
}
|
||||
var i, ret, addresses, addr, name, aspec, local, domain;
|
||||
var addresses, groupsAndMailboxes, i, groupOrMailbox, result;
|
||||
if (ast === null) {
|
||||
return null;
|
||||
}
|
||||
ret = { ast: ast };
|
||||
addresses = findAllNodes('address', ast);
|
||||
ret.addresses = [];
|
||||
for (i = 0; i < addresses.length; i += 1) {
|
||||
addr = addresses[i];
|
||||
name = findNode('display-name', addr);
|
||||
aspec = findNode('addr-spec', addr);
|
||||
local = findNode('local-part', aspec);
|
||||
domain = findNode('domain', aspec);
|
||||
ret.addresses.push({
|
||||
node: addr,
|
||||
parts: {
|
||||
name: name,
|
||||
address: aspec,
|
||||
local: local,
|
||||
domain: domain
|
||||
},
|
||||
name: grabSemantic(name),
|
||||
address: grabSemantic(aspec),
|
||||
local: grabSemantic(local),
|
||||
domain: grabSemantic(domain)
|
||||
});
|
||||
}
|
||||
addresses = [];
|
||||
|
||||
if (opts.simple) {
|
||||
ret = ret.addresses;
|
||||
for (i = 0; i < ret.length; i += 1) {
|
||||
delete ret[i].node;
|
||||
// An address is a 'group' (i.e. a list of mailboxes) or a 'mailbox'.
|
||||
groupsAndMailboxes = findAllNodesNoChildren(['group', 'mailbox'], ast);
|
||||
for (i = 0; i < groupsAndMailboxes.length; i += 1) {
|
||||
groupOrMailbox = groupsAndMailboxes[i];
|
||||
if (groupOrMailbox.name === 'group') {
|
||||
addresses.push(giveResultGroup(groupOrMailbox));
|
||||
} else if (groupOrMailbox.name === 'mailbox') {
|
||||
addresses.push(giveResultMailbox(groupOrMailbox));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
|
||||
result = {
|
||||
ast: ast,
|
||||
addresses: addresses,
|
||||
};
|
||||
if (opts.simple) {
|
||||
result = simplifyResult(result);
|
||||
}
|
||||
if (opts.oneResult) {
|
||||
return oneResult(result);
|
||||
}
|
||||
if (opts.simple) {
|
||||
return result && result.addresses;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function giveResultGroup(group) {
|
||||
var i;
|
||||
var groupName = findNode('display-name', group);
|
||||
var groupResultMailboxes = [];
|
||||
var mailboxes = findAllNodesNoChildren(['mailbox'], group);
|
||||
for (i = 0; i < mailboxes.length; i += 1) {
|
||||
groupResultMailboxes.push(giveResultMailbox(mailboxes[i]));
|
||||
}
|
||||
return {
|
||||
node: group,
|
||||
parts: {
|
||||
name: groupName,
|
||||
},
|
||||
type: group.name, // 'group'
|
||||
name: grabSemantic(groupName),
|
||||
addresses: groupResultMailboxes,
|
||||
};
|
||||
}
|
||||
|
||||
function giveResultMailbox(mailbox) {
|
||||
var name = findNode('display-name', mailbox);
|
||||
var aspec = findNode('addr-spec', mailbox);
|
||||
var comments = findAllNodes('cfws', mailbox);
|
||||
|
||||
var local = findNode('local-part', aspec);
|
||||
var domain = findNode('domain', aspec);
|
||||
return {
|
||||
node: mailbox,
|
||||
parts: {
|
||||
name: name,
|
||||
address: aspec,
|
||||
local: local,
|
||||
domain: domain,
|
||||
comments: comments
|
||||
},
|
||||
type: mailbox.name, // 'mailbox'
|
||||
name: grabSemantic(name),
|
||||
address: grabSemantic(aspec),
|
||||
local: grabSemantic(local),
|
||||
domain: grabSemantic(domain),
|
||||
groupName: grabSemantic(mailbox.groupName),
|
||||
};
|
||||
}
|
||||
|
||||
function grabSemantic(n) {
|
||||
return n !== null && n !== undefined ? n.semantic : null;
|
||||
}
|
||||
|
||||
function simplifyResult(result) {
|
||||
var i;
|
||||
if (result && result.addresses) {
|
||||
for (i = 0; i < result.addresses.length; i += 1) {
|
||||
delete result.addresses[i].node;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function oneResult(result) {
|
||||
if (!result) { return null; }
|
||||
if (!opts.partial && result.addresses.length > 1) { return null; }
|
||||
return result.addresses && result.addresses[0];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////
|
||||
|
||||
var parseString, pos, len, parsed;
|
||||
var parseString, pos, len, parsed, startProduction;
|
||||
|
||||
opts = handleOpts(opts, {});
|
||||
if (opts === null) { return null; }
|
||||
|
||||
parseString = opts.input;
|
||||
|
||||
startProduction = {
|
||||
'address': address,
|
||||
'address-list': addressList,
|
||||
'angle-addr': angleAddr,
|
||||
'from': fromSpec,
|
||||
'group': group,
|
||||
'mailbox': mailbox,
|
||||
'mailbox-list': mailboxList,
|
||||
'reply-to': replyToSpec,
|
||||
'sender': senderSpec,
|
||||
}[opts.startAt] || addressList;
|
||||
|
||||
if (!opts.strict) {
|
||||
initialize();
|
||||
opts.strict = true;
|
||||
parsed = addressList(parseString);
|
||||
parsed = startProduction(parseString);
|
||||
if (opts.partial || !inStr()) {
|
||||
return giveResult(parsed);
|
||||
}
|
||||
|
@ -859,46 +967,51 @@ function parse5322(opts) {
|
|||
}
|
||||
|
||||
initialize();
|
||||
parsed = addressList(parseString);
|
||||
parsed = startProduction(parseString);
|
||||
if (!opts.partial && inStr()) { return null; }
|
||||
return giveResult(parsed);
|
||||
}
|
||||
|
||||
function parseOneAddressSimple(opts) {
|
||||
var result;
|
||||
|
||||
opts = handleOpts(opts, {
|
||||
return parse5322(handleOpts(opts, {
|
||||
oneResult: true,
|
||||
rfc6532: true,
|
||||
simple: true
|
||||
});
|
||||
if (opts === null) { return null; }
|
||||
|
||||
result = parse5322(opts);
|
||||
|
||||
if ((!result) ||
|
||||
(!opts.partial &&
|
||||
(opts.simple && result.length > 1) ||
|
||||
(!opts.simple && result.addresses.length > 1))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return opts.simple ?
|
||||
result && result[0] :
|
||||
result && result.addresses && result.addresses[0];
|
||||
simple: true,
|
||||
startAt: 'address-list',
|
||||
}));
|
||||
}
|
||||
|
||||
function parseAddressListSimple(opts) {
|
||||
var result;
|
||||
|
||||
opts = handleOpts(opts, {
|
||||
return parse5322(handleOpts(opts, {
|
||||
rfc6532: true,
|
||||
simple: true
|
||||
});
|
||||
if (opts === null) { return null; }
|
||||
simple: true,
|
||||
startAt: 'address-list',
|
||||
}));
|
||||
}
|
||||
|
||||
result = parse5322(opts);
|
||||
function parseFromSimple(opts) {
|
||||
return parse5322(handleOpts(opts, {
|
||||
rfc6532: true,
|
||||
simple: true,
|
||||
startAt: 'from',
|
||||
}));
|
||||
}
|
||||
|
||||
return opts.simple ? result : result.addresses;
|
||||
function parseSenderSimple(opts) {
|
||||
return parse5322(handleOpts(opts, {
|
||||
oneResult: true,
|
||||
rfc6532: true,
|
||||
simple: true,
|
||||
startAt: 'sender',
|
||||
}));
|
||||
}
|
||||
|
||||
function parseReplyToSimple(opts) {
|
||||
return parse5322(handleOpts(opts, {
|
||||
rfc6532: true,
|
||||
simple: true,
|
||||
startAt: 'reply-to',
|
||||
}));
|
||||
}
|
||||
|
||||
function handleOpts(opts, defs) {
|
||||
|
@ -926,24 +1039,28 @@ function handleOpts(opts, defs) {
|
|||
if (!defs) { return null; }
|
||||
|
||||
defaults = {
|
||||
rfc6532: false,
|
||||
oneResult: false,
|
||||
partial: false,
|
||||
rejectTLD: false,
|
||||
rfc6532: false,
|
||||
simple: false,
|
||||
startAt: 'address-list',
|
||||
strict: false,
|
||||
rejectTLD: false
|
||||
};
|
||||
|
||||
for (o in defaults) {
|
||||
if (isNullUndef(opts[o])) {
|
||||
opts[o] = !isNullUndef(defs[o]) ? defs[o] : defaults[o];
|
||||
}
|
||||
opts[o] = !!opts[o];
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
parse5322.parseOneAddress = parseOneAddressSimple;
|
||||
parse5322.parseAddressList = parseAddressListSimple;
|
||||
parse5322.parseFrom = parseFromSimple;
|
||||
parse5322.parseSender = parseSenderSimple;
|
||||
parse5322.parseReplyTo = parseReplyToSimple;
|
||||
|
||||
// in Zammad context, go back to non CommonJS
|
||||
// if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
|
||||
|
|
|
@ -289,15 +289,16 @@
|
|||
var result = e.target.result
|
||||
var img = document.createElement('img')
|
||||
img.src = result
|
||||
maxWidth = _this.$element.width() || 500
|
||||
scaleFactor = 2
|
||||
//scaleFactor = 1
|
||||
//if (window.isRetina && window.isRetina()) {
|
||||
// scaleFactor = 2
|
||||
//}
|
||||
|
||||
insert = function(dataUrl, width, height, isRetina) {
|
||||
insert = function(dataUrl, width, height, isResized) {
|
||||
//console.log('dataUrl', dataUrl)
|
||||
|
||||
// adapt image if we are on retina devices
|
||||
if (!isRetina && window.isRetina && window.isRetina()) {
|
||||
width = width / 2
|
||||
height = height / 2
|
||||
}
|
||||
//console.log('scaleFactor', scaleFactor, isResized, maxWidth, width, height)
|
||||
_this.log('image inserted')
|
||||
result = dataUrl
|
||||
if (_this.options.imageWidth == 'absolute') {
|
||||
|
@ -310,7 +311,7 @@
|
|||
}
|
||||
|
||||
// resize if to big
|
||||
App.ImageService.resize(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
|
||||
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
|
||||
}
|
||||
reader.readAsDataURL(imageFile)
|
||||
imageInserted = true
|
||||
|
@ -416,17 +417,18 @@
|
|||
var result = e.target.result
|
||||
var img = document.createElement('img')
|
||||
img.src = result
|
||||
maxWidth = _this.$element.width() || 500
|
||||
scaleFactor = 2
|
||||
//scaleFactor = 1
|
||||
//if (window.isRetina && window.isRetina()) {
|
||||
// scaleFactor = 2
|
||||
//}
|
||||
|
||||
//Insert the image at the carat
|
||||
insert = function(dataUrl, width, height, isRetina) {
|
||||
|
||||
// adapt image if we are on retina devices
|
||||
if (!isRetina && window.isRetina && window.isRetina()) {
|
||||
width = width / 2
|
||||
height = height / 2
|
||||
}
|
||||
insert = function(dataUrl, width, height, isResized) {
|
||||
|
||||
//console.log('dataUrl', dataUrl)
|
||||
//console.log('scaleFactor', scaleFactor, isResized, maxWidth, width, height)
|
||||
_this.log('image inserted')
|
||||
result = dataUrl
|
||||
if (_this.options.imageWidth == 'absolute') {
|
||||
|
@ -454,7 +456,7 @@
|
|||
}
|
||||
|
||||
// resize if to big
|
||||
App.ImageService.resize(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
|
||||
App.ImageService.resize(img.src, maxWidth, 'auto', scaleFactor, 'image/jpeg', 'auto', insert)
|
||||
})
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
|
|
@ -74,8 +74,9 @@ window.word_filter = function(editor){
|
|||
}
|
||||
})
|
||||
|
||||
$('[style]', editor).removeAttr('style');
|
||||
$('[align]', editor).removeAttr('align');
|
||||
// style and align is handled by utils.coffee it self, don't clean it here
|
||||
//$('[style]', editor).removeAttr('style');
|
||||
//$('[align]', editor).removeAttr('align');
|
||||
$('span', editor).replaceWith(function() {return $(this).contents();});
|
||||
$('span:empty', editor).remove();
|
||||
$("[class^='Mso']", editor).removeAttr('class');
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
- allow custom template as options parameter
|
||||
- fix that place method doesn't think that the container is the window, but rather the real window is the window
|
||||
- added rerender method to show correct today if task is longer open the 24 hours
|
||||
- scroll into view
|
||||
*/
|
||||
|
||||
(function(factory){
|
||||
|
@ -515,7 +516,9 @@
|
|||
)
|
||||
)
|
||||
this.setValue();
|
||||
this._trigger('hide');
|
||||
// 2018-01-22 trigger locale hide event - conflicts with modal hide
|
||||
//this._trigger('hide');
|
||||
this._trigger('hide.bs.datepicker');
|
||||
return this;
|
||||
},
|
||||
|
||||
|
@ -757,6 +760,16 @@
|
|||
zIndex: zIndex
|
||||
});
|
||||
}
|
||||
|
||||
// adjust scroll of scrollParent
|
||||
var scrollParent = this.picker.scrollParent();
|
||||
var bottomEdge = offset.top + height + this.picker.outerHeight();
|
||||
var scrollBottomEdge = scrollParent.scrollTop() + scrollParent.height();
|
||||
|
||||
if(bottomEdge > scrollBottomEdge){
|
||||
scrollParent.scrollTop(scrollParent.scrollTop() + (bottomEdge - scrollBottomEdge) + 10);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
|
|
@ -387,15 +387,17 @@ set new attributes of model (remove already available attributes)
|
|||
=>
|
||||
return if _.isEmpty(@SUBSCRIPTION_COLLECTION)
|
||||
App.Log.debug('Model', "server notify collection change #{@className}")
|
||||
@fetchFull(
|
||||
->
|
||||
clear: true
|
||||
)
|
||||
callback = =>
|
||||
@fetchFull(
|
||||
->
|
||||
clear: true
|
||||
)
|
||||
App.Delay.set(callback, 200, "full-#{@className}")
|
||||
|
||||
"Collection::Subscribe::#{@className}"
|
||||
)
|
||||
|
||||
key = @className + '-' + Math.floor( Math.random() * 99999 )
|
||||
key = "#{@className}-#{Math.floor(Math.random() * 99999)}"
|
||||
@SUBSCRIPTION_COLLECTION[key] = callback
|
||||
|
||||
# fetch init collection
|
||||
|
|
|
@ -1,16 +1,260 @@
|
|||
class App.Chat extends App.Model
|
||||
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'note'
|
||||
@configure 'Chat', 'name', 'active', 'public', 'max_queue', 'block_ip', 'block_country', 'note'
|
||||
@extend Spine.Model.Ajax
|
||||
@url: @apiPath + '/chats'
|
||||
@countries:
|
||||
AF: 'Afghanistan'
|
||||
AL: 'Albania'
|
||||
DZ: 'Algeria'
|
||||
AS: 'American Samoa'
|
||||
AD: 'Andorra'
|
||||
AO: 'Angola'
|
||||
AI: 'Anguilla'
|
||||
AQ: 'Antarctica'
|
||||
AG: 'Antigua And Barbuda'
|
||||
AR: 'Argentina'
|
||||
AM: 'Armenia'
|
||||
AW: 'Aruba'
|
||||
AU: 'Australia'
|
||||
AT: 'Austria'
|
||||
AZ: 'Azerbaijan'
|
||||
BS: 'Bahamas'
|
||||
BH: 'Bahrain'
|
||||
BD: 'Bangladesh'
|
||||
BB: 'Barbados'
|
||||
BY: 'Belarus'
|
||||
BE: 'Belgium'
|
||||
BZ: 'Belize'
|
||||
BJ: 'Benin'
|
||||
BM: 'Bermuda'
|
||||
BT: 'Bhutan'
|
||||
BO: 'Bolivia'
|
||||
BA: 'Bosnia And Herzegovina'
|
||||
BW: 'Botswana'
|
||||
BV: 'Bouvet Island'
|
||||
BR: 'Brazil'
|
||||
IO: 'British Indian Ocean Territory'
|
||||
BN: 'Brunei Darussalam'
|
||||
BG: 'Bulgaria'
|
||||
BF: 'Burkina Faso'
|
||||
BI: 'Burundi'
|
||||
KH: 'Cambodia'
|
||||
CM: 'Cameroon'
|
||||
CA: 'Canada'
|
||||
CV: 'Cape Verde'
|
||||
KY: 'Cayman Islands'
|
||||
CF: 'Central African Republic'
|
||||
TD: 'Chad'
|
||||
CL: 'Chile'
|
||||
CN: 'China'
|
||||
CX: 'Christmas Island'
|
||||
CC: 'Cocos (keeling) Islands'
|
||||
CO: 'Colombia'
|
||||
KM: 'Comoros'
|
||||
CG: 'Congo'
|
||||
CD: 'Congo, The Democratic Republic Of The'
|
||||
CK: 'Cook Islands'
|
||||
CR: 'Costa Rica'
|
||||
CI: 'Cote D\'ivoire'
|
||||
HR: 'Croatia'
|
||||
CU: 'Cuba'
|
||||
CY: 'Cyprus'
|
||||
CZ: 'Czech Republic'
|
||||
DK: 'Denmark'
|
||||
DJ: 'Djibouti'
|
||||
DM: 'Dominica'
|
||||
DO: 'Dominican Republic'
|
||||
TP: 'East Timor'
|
||||
EC: 'Ecuador'
|
||||
EG: 'Egypt'
|
||||
SV: 'El Salvador'
|
||||
GQ: 'Equatorial Guinea'
|
||||
ER: 'Eritrea'
|
||||
EE: 'Estonia'
|
||||
ET: 'Ethiopia'
|
||||
FK: 'Falkland Islands (malvinas)'
|
||||
FO: 'Faroe Islands'
|
||||
FJ: 'Fiji'
|
||||
FI: 'Finland'
|
||||
FR: 'France'
|
||||
GF: 'French Guiana'
|
||||
PF: 'French Polynesia'
|
||||
TF: 'French Southern Territories'
|
||||
GA: 'Gabon'
|
||||
GM: 'Gambia'
|
||||
GE: 'Georgia'
|
||||
DE: 'Germany'
|
||||
GH: 'Ghana'
|
||||
GI: 'Gibraltar'
|
||||
GR: 'Greece'
|
||||
GL: 'Greenland'
|
||||
GD: 'Grenada'
|
||||
GP: 'Guadeloupe'
|
||||
GU: 'Guam'
|
||||
GT: 'Guatemala'
|
||||
GN: 'Guinea'
|
||||
GW: 'Guinea-bissau'
|
||||
GY: 'Guyana'
|
||||
HT: 'Haiti'
|
||||
HM: 'Heard Island And Mcdonald Islands'
|
||||
VA: 'Holy See (vatican City State)'
|
||||
HN: 'Honduras'
|
||||
HK: 'Hong Kong'
|
||||
HU: 'Hungary'
|
||||
IS: 'Iceland'
|
||||
IN: 'India'
|
||||
ID: 'Indonesia'
|
||||
IR: 'Iran, Islamic Republic Of'
|
||||
IQ: 'Iraq'
|
||||
IE: 'Ireland'
|
||||
IL: 'Israel'
|
||||
IT: 'Italy'
|
||||
JM: 'Jamaica'
|
||||
JP: 'Japan'
|
||||
JO: 'Jordan'
|
||||
KZ: 'Kazakstan'
|
||||
KE: 'Kenya'
|
||||
KI: 'Kiribati'
|
||||
KP: 'Korea, Democratic People\'s Republic Of'
|
||||
KR: 'Korea, Republic Of'
|
||||
KV: 'Kosovo'
|
||||
KW: 'Kuwait'
|
||||
KG: 'Kyrgyzstan'
|
||||
LA: 'Lao People\'s Democratic Republic'
|
||||
LV: 'Latvia'
|
||||
LB: 'Lebanon'
|
||||
LS: 'Lesotho'
|
||||
LR: 'Liberia'
|
||||
LY: 'Libyan Arab Jamahiriya'
|
||||
LI: 'Liechtenstein'
|
||||
LT: 'Lithuania'
|
||||
LU: 'Luxembourg'
|
||||
MO: 'Macau'
|
||||
MK: 'Macedonia, The Former Yugoslav Republic Of'
|
||||
MG: 'Madagascar'
|
||||
MW: 'Malawi'
|
||||
MY: 'Malaysia'
|
||||
MV: 'Maldives'
|
||||
ML: 'Mali'
|
||||
MT: 'Malta'
|
||||
MH: 'Marshall Islands'
|
||||
MQ: 'Martinique'
|
||||
MR: 'Mauritania'
|
||||
MU: 'Mauritius'
|
||||
YT: 'Mayotte'
|
||||
MX: 'Mexico'
|
||||
FM: 'Micronesia, Federated States Of'
|
||||
MD: 'Moldova, Republic Of'
|
||||
MC: 'Monaco'
|
||||
MN: 'Mongolia'
|
||||
MS: 'Montserrat'
|
||||
ME: 'Montenegro'
|
||||
MA: 'Morocco'
|
||||
MZ: 'Mozambique'
|
||||
MM: 'Myanmar'
|
||||
NA: 'Namibia'
|
||||
NR: 'Nauru'
|
||||
NP: 'Nepal'
|
||||
NL: 'Netherlands'
|
||||
AN: 'Netherlands Antilles'
|
||||
NC: 'New Caledonia'
|
||||
NZ: 'New Zealand'
|
||||
NI: 'Nicaragua'
|
||||
NE: 'Niger'
|
||||
NG: 'Nigeria'
|
||||
NU: 'Niue'
|
||||
NF: 'Norfolk Island'
|
||||
MP: 'Northern Mariana Islands'
|
||||
NO: 'Norway'
|
||||
OM: 'Oman'
|
||||
PK: 'Pakistan'
|
||||
PW: 'Palau'
|
||||
PS: 'Palestinian Territory, Occupied'
|
||||
PA: 'Panama'
|
||||
PG: 'Papua New Guinea'
|
||||
PY: 'Paraguay'
|
||||
PE: 'Peru'
|
||||
PH: 'Philippines'
|
||||
PN: 'Pitcairn'
|
||||
PL: 'Poland'
|
||||
PT: 'Portugal'
|
||||
PR: 'Puerto Rico'
|
||||
QA: 'Qatar'
|
||||
RE: 'Reunion'
|
||||
RO: 'Romania'
|
||||
RU: 'Russian Federation'
|
||||
RW: 'Rwanda'
|
||||
SH: 'Saint Helena'
|
||||
KN: 'Saint Kitts And Nevis'
|
||||
LC: 'Saint Lucia'
|
||||
PM: 'Saint Pierre And Miquelon'
|
||||
VC: 'Saint Vincent And The Grenadines'
|
||||
WS: 'Samoa'
|
||||
SM: 'San Marino'
|
||||
ST: 'Sao Tome And Principe'
|
||||
SA: 'Saudi Arabia'
|
||||
SN: 'Senegal'
|
||||
RS: 'Serbia'
|
||||
SC: 'Seychelles'
|
||||
SL: 'Sierra Leone'
|
||||
SG: 'Singapore'
|
||||
SK: 'Slovakia'
|
||||
SI: 'Slovenia'
|
||||
SB: 'Solomon Islands'
|
||||
SO: 'Somalia'
|
||||
ZA: 'South Africa'
|
||||
GS: 'South Georgia And The South Sandwich Islands'
|
||||
ES: 'Spain'
|
||||
LK: 'Sri Lanka'
|
||||
SD: 'Sudan'
|
||||
SR: 'Suriname'
|
||||
SJ: 'Svalbard And Jan Mayen'
|
||||
SZ: 'Swaziland'
|
||||
SE: 'Sweden'
|
||||
CH: 'Switzerland'
|
||||
SY: 'Syrian Arab Republic'
|
||||
TW: 'Taiwan, Province Of China'
|
||||
TJ: 'Tajikistan'
|
||||
TZ: 'Tanzania, United Republic Of'
|
||||
TH: 'Thailand'
|
||||
TG: 'Togo'
|
||||
TK: 'Tokelau'
|
||||
TO: 'Tonga'
|
||||
TT: 'Trinidad And Tobago'
|
||||
TN: 'Tunisia'
|
||||
TR: 'Turkey'
|
||||
TM: 'Turkmenistan'
|
||||
TC: 'Turks And Caicos Islands'
|
||||
TV: 'Tuvalu'
|
||||
UG: 'Uganda'
|
||||
UA: 'Ukraine'
|
||||
AE: 'United Arab Emirates'
|
||||
GB: 'United Kingdom'
|
||||
US: 'United States'
|
||||
UM: 'United States Minor Outlying Islands'
|
||||
UY: 'Uruguay'
|
||||
UZ: 'Uzbekistan'
|
||||
VU: 'Vanuatu'
|
||||
VE: 'Venezuela'
|
||||
VN: 'Viet Nam'
|
||||
VG: 'Virgin Islands, British'
|
||||
VI: 'Virgin Islands, U.s.'
|
||||
WF: 'Wallis And Futuna'
|
||||
EH: 'Western Sahara'
|
||||
YE: 'Yemen'
|
||||
ZM: 'Zambia'
|
||||
ZW: 'Zimbabwe'
|
||||
|
||||
@configure_attributes = [
|
||||
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
|
||||
{ name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true },
|
||||
#{ name: 'public', display: 'Public', tag: 'boolean', default: true },
|
||||
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
|
||||
{ name: 'max_queue', display: 'Max. clients in waitlist', tag: 'input', default: 2 },
|
||||
{ name: 'block_ip', display: 'Blocked IPs (separated by ;)', tag: 'input', default: '', null: true },
|
||||
{ name: 'block_country', display: 'Blocked countries', tag: 'column_select', multiple: true, null: true, default: '', options: @countries, seperator: ';' },
|
||||
{ name: 'active', display: 'Active', tag: 'active', default: true },
|
||||
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
|
||||
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 },
|
||||
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
|
||||
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 },
|
||||
]
|
||||
|
||||
|
|
32
app/assets/javascripts/app/models/chat_sessions.coffee
Normal file
32
app/assets/javascripts/app/models/chat_sessions.coffee
Normal 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'
|
|
@ -1,5 +1,5 @@
|
|||
class App.Overview extends App.Model
|
||||
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'order', 'group_by', 'active', 'updated_at'
|
||||
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'active'
|
||||
@extend Spine.Model.Ajax
|
||||
@url: @apiPath + '/overviews'
|
||||
@configure_attributes = [
|
||||
|
|
|
@ -200,6 +200,26 @@ class App.Ticket extends App.Model
|
|||
result = true if objectValue.toString().match(contains_regex)
|
||||
else if condition.operator == 'contains not'
|
||||
result = true if !objectValue.toString().match(contains_regex)
|
||||
else if condition.operator == 'contains all'
|
||||
result = true
|
||||
for loopConditionValue in conditionValue
|
||||
if !_.contains(objectValue, loopConditionValue)
|
||||
result = false
|
||||
else if condition.operator == 'contains one'
|
||||
result = false
|
||||
for loopConditionValue in conditionValue
|
||||
if _.contains(objectValue, loopConditionValue)
|
||||
result = true
|
||||
else if condition.operator == 'contains all not'
|
||||
result = true
|
||||
for loopObjectValue in objectValue
|
||||
if _.contains(conditionValue, loopObjectValue)
|
||||
result = false
|
||||
else if condition.operator == 'contains one not'
|
||||
result = false
|
||||
for loopObjectValue in objectValue
|
||||
if !_.contains(conditionValue, loopObjectValue)
|
||||
result = true
|
||||
else if condition.operator == 'is'
|
||||
result = true if objectValue.toString().trim().toLowerCase() is loopConditionValue.toString().trim().toLowerCase()
|
||||
else if condition.operator == 'is not'
|
||||
|
@ -224,3 +244,19 @@ class App.Ticket extends App.Model
|
|||
throw "Unknown operator: #{condition.operator}"
|
||||
|
||||
result
|
||||
|
||||
editable: (permission = 'change') ->
|
||||
user_id = App.Session.get('id')
|
||||
return true if user_id is @customer_id
|
||||
group_ids = App.Session.get('group_ids')
|
||||
if group_ids
|
||||
return true if group_ids[@group_id] && (_.include(group_ids[@group_id], permission) || _.include(group_ids[@group_id], 'full'))
|
||||
role_ids = App.Session.get('role_ids')
|
||||
if role_ids
|
||||
for role_id in role_ids
|
||||
if App.Role.exists(role_id)
|
||||
role = App.Role.find(role_id)
|
||||
if role.group_ids
|
||||
return true if role.group_ids[@group_id] && (_.include(role.group_ids[@group_id], permission) || _.include(role.group_ids[@group_id], 'full'))
|
||||
false
|
||||
|
||||
|
|
|
@ -3,28 +3,31 @@
|
|||
<div class="newTicket">
|
||||
<div class="box box--newTicket">
|
||||
<div class="page-header">
|
||||
<h1><%- @T( @head ) %></h1>
|
||||
<h1><%- @T(@head) %></h1>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<ul class="tabs type-tabs">
|
||||
|
||||
<li class="tab u-textTruncate" data-type="phone-in">
|
||||
<%- @Icon('received-calls', 'tab-icon') %>
|
||||
<%- @T('Received Call') %>
|
||||
</li>
|
||||
|
||||
<li class="tab u-textTruncate" data-type="phone-out">
|
||||
<%- @Icon('outbound-calls', 'tab-icon') %>
|
||||
<%- @T('Outbound Call') %>
|
||||
</li>
|
||||
|
||||
<li class="tab u-textTruncate" data-type="email-out">
|
||||
<li class="tab u-textTruncate" data-type="email-out">
|
||||
<%- @Icon('email', 'tab-icon') %>
|
||||
<%- @T('Send Email') %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<% if !_.isEmpty(@C('ui_ticket_create_notes')): %>
|
||||
<% for type, note of @C('ui_ticket_create_notes'): %>
|
||||
<div class="alert alert--warning js-note" role="alert" data-type="<%= type %>"><%- @T(note) %></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<form role="form" class="ticket-create">
|
||||
<input type="hidden" name="formSenderType"/>
|
||||
<input type="hidden" name="form_id" value="<%= @form_id %>"/>
|
||||
|
@ -46,7 +49,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabsSidebar vertical"></div>
|
||||
</div>
|
||||
<!--
|
||||
|
|
|
@ -132,7 +132,7 @@
|
|||
|
||||
<p><%- @T('You need to add the following Javascript code snippet to your web page') %>:</p>
|
||||
|
||||
<pre><code class="language-html js-paramsBlock"><button id="feedback-form">Feedback</button>
|
||||
<pre class="js-modal"><code class="language-html js-code js-paramsBlock"><button id="feedback-form">Feedback</button>
|
||||
|
||||
<script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"></script>
|
||||
|
||||
|
@ -144,3 +144,16 @@ $(function() {
|
|||
});
|
||||
</script></code></pre>
|
||||
</div>
|
||||
|
||||
<pre class="js-inlineForm"><code class="language-html js-code js-paramsBlock"><div id="feedback-form">form will be placed in here</div>
|
||||
|
||||
<script id="zammad_form_script" src="<%= @baseurl %>/assets/form/form.js"></script>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('#feedback-form').ZammadForm({
|
||||
<span class="js-modal-params"></span>
|
||||
});
|
||||
});
|
||||
</script></code></pre>
|
||||
</div>
|
|
@ -1,5 +1,3 @@
|
|||
<!--
|
||||
<div class="chat-footer">
|
||||
<div class="btn btn--primary js-createTicket">Turn chat into ticket</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="btn btn--primary js-createTicket"><%- @T('Turn chat into ticket') %></div>
|
||||
</div>
|
|
@ -7,9 +7,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="chat-name">
|
||||
<%= @name %> <div class="status-badge js-info">
|
||||
<div class="info-badge"><%- @Icon('info') %></div>
|
||||
</div>
|
||||
<span class="js-name js-info u-clickable"><%= @name %><span> #<%= @session.id %>
|
||||
</div>
|
||||
<div class="chat-disconnect js-disconnect">
|
||||
<div class="btn btn--action btn--small"><%- @T('disconnect') %></div>
|
||||
|
@ -24,6 +22,25 @@
|
|||
</div>
|
||||
<div class="chat-body-holder js-scrollHolder">
|
||||
<div class="chat-body js-body"></div>
|
||||
<div class="chat-body js-meta hidden">
|
||||
<% if @session: %>
|
||||
<ul>
|
||||
<li><%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %></li>
|
||||
<% if @session && @session.preferences: %>
|
||||
<% if @session.preferences.geo_ip: %>
|
||||
<li>GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %></li>
|
||||
<% end %>
|
||||
<% if @session.preferences.remote_ip: %>
|
||||
<li>IP: <%= @session.preferences.remote_ip %></li>
|
||||
<% end %>
|
||||
<% if @session.preferences.dns_name: %>
|
||||
<li>DNS: <%= @session.preferences.dns_name %></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<form class="js-metaForm" style="max-width: 200px; width: 100%;"></form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-controls">
|
||||
<div class="chat-input">
|
||||
|
|
|
@ -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>
|
|
@ -1,7 +1,7 @@
|
|||
<div class="attachment">
|
||||
<div class="attachment-name"><%= @fileName %></div>
|
||||
<div class="attachment-size"><%= @fileSize %></div>
|
||||
<div class="attachment-delete js-delete" data-id="<%= @store_id %>">
|
||||
<div class="attachment-name"><%= @filename %></div>
|
||||
<div class="attachment-size"><%= @humanFileSize(@size) %></div>
|
||||
<div class="attachment-delete js-delete" data-id="<%= @id %>">
|
||||
<%- @Icon('diagonal-cross') %><%- @T('Delete File') %>
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +1,6 @@
|
|||
<% if @attribute.seperator: %>
|
||||
<input class="js-shadow hide" id="<%= @attribute.id %>" name="<%= @attribute.name %>" value="<%= @attribute.value %>">
|
||||
<% else: %>
|
||||
<select
|
||||
class="columnSelect-shadow js-shadow"
|
||||
id="<%= @attribute.id %>"
|
||||
|
@ -11,6 +14,7 @@
|
|||
<option value="<%= option.value %>" <%= ' selected' if option.selected %>><%= option.name %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<% end %>
|
||||
<div class="columnSelect-column columnSelect-column--selected js-selected" data-name="<%= @attribute.name %>">
|
||||
<div class="u-placeholder u-unselectable js-placeholder<%= ' is-hidden' if @values.length %>"><%- @T('Nothing selected') %></div>
|
||||
<% for option in @attribute.options: %>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<% for item in @items: %>
|
||||
<div class="sidebar bottom-form-shadow flex hide" data-tab="<%= item.name %>">
|
||||
<div class="sidebar-header">
|
||||
<h2 class="sidebar-header-headline js-headline"><%- @T(item.head) %></h2>
|
||||
<h2 class="sidebar-header-headline js-headline"><%- @T(item.sidebarHead) %></h2>
|
||||
<div class="sidebar-header-actions js-actions"></div>
|
||||
<div class="tabsSidebar-close">
|
||||
<%- @Icon('long-arrow-right') %>
|
||||
|
@ -13,8 +13,6 @@
|
|||
<% end %>
|
||||
<div class="tabsSidebar-tabs" style="<%- if @dir is 'rtl' then 'margin-right' else 'margin-left' %>: -<%- @scrollbarWidth %>px">
|
||||
<% for item in @items: %>
|
||||
<div class="tabsSidebar-tab" data-tab="<%= item.name %>">
|
||||
<%- @Icon(item.icon) %>
|
||||
</div>
|
||||
<div class="tabsSidebar-tab" data-tab="<%= item.name %>"></div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -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) %>
|
|
@ -1,5 +1,5 @@
|
|||
<div class="js-pager"></div>
|
||||
<table class="table table-hover<%- " #{@class}" if @class %>">
|
||||
<table class="table table-hover<% if @class: %> <%= @class %><% end %>">
|
||||
<thead>
|
||||
<tr>
|
||||
<% if @sortable: %>
|
||||
|
@ -19,14 +19,10 @@
|
|||
<th style="width: 40px" class="table-radio"></th>
|
||||
<% end %>
|
||||
<% for header, i in @headers: %>
|
||||
<th class="js-tableHead<%= " #{ header.className }" if header.className %><%= " align-#{ header.align }" if header.align %>" style="<% if header.displayWidth: %>width:<%= header.displayWidth %>px<% end %>" data-column-key="<%= header.name %>">
|
||||
<th class="js-tableHead<% if header.className: %> <%= header.className %><% end %><% if header.align: %> align-<%= header.align %><% end %>" style="<% if header.displayWidth: %>width:<%= header.displayWidth %>px<% end %>" data-column-key="<%= header.name %>">
|
||||
<div class="table-column-head<%= ' js-sort' if @tableId %>">
|
||||
<div class="table-column-title"><%- @T(header.display) %></div>
|
||||
<div class="table-column-sortIcon">
|
||||
<% if header.sortOrderIcon: %>
|
||||
<%- @Icon(header.sortOrderIcon[0], header.sortOrderIcon[1]) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="table-column-sortIcon"><% if header.sortOrderIcon: %><%- @Icon(header.sortOrderIcon[0], header.sortOrderIcon[1]) %><% end %></div>
|
||||
</div>
|
||||
<% if @tableId && !header.unresizable && i < @headers.length - 1: %>
|
||||
<div class="table-col-resize js-col-resize"></div>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</td>
|
||||
<% end %>
|
||||
<% for header in @headers: %>
|
||||
<% value = @P(@object, header.name, @attributes) %>
|
||||
<% value = @P(@object, header.name, @attributes, true) %>
|
||||
<% if @callbacks: %>
|
||||
<% for attribute, callbacksAll of @callbacks: %>
|
||||
<% if attribute is header.name: %>
|
||||
|
@ -31,7 +31,7 @@
|
|||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<td<%- " class='#{ header.parentClass }'" if header.parentClass %><%- " title='#{ header.title }'" if header.title %><%- " style='text-align:#{ header.align }'" if header.align %>>
|
||||
<td<% if header.parentClass: %> class="<%= header.parentClass %>"<% end %><% if header.title: %> title="<%= header.title %>"<% end %><% if header.align: %> style="text-align:<%= header.align %>"<% end %>>
|
||||
<% if header.name is 'icon': %>
|
||||
<%- @Icon('task-state', header.class) %>
|
||||
<% else if header.icon: %>
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
<p>
|
||||
<%- @T('Download and install the %s Migration Plugin on your %s instance.', 'OTRS', 'OTRS') %>:
|
||||
</p>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/617/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 5') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/383/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 4') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://portal.znuny.com/api/addon_repos/public/287/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 3.3-3.1') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/1085/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 6') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/617/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 5') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/383/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 4') %></a>
|
||||
<a class="btn btn--primary btn--download js-download" target=_blank href="https://addons.znuny.com/api/addon_repos/public/287/latest" download><%- @Icon('download') %> <%- @T('Migration Plugin for %s', 'OTRS 3.3-3.1') %></a>
|
||||
</div>
|
||||
<div class="wizard-controls horizontal center">
|
||||
<a class="btn btn--text btn--secondary" href="#import"><%- @T('Go Back') %></a>
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
<div class="alert alert--info hide js-ticket-count-info" role="alert"><%- @T("There are more than 1000 tickets in the Zendesk system. Due to API rate limit restrictions we can't get the exact number of tickets yet and have to fetch them in batches of 1000. This might take some time, better grab a cup of coffee. The total number of tickets gets updated as soon as the currently known number is surpassed.") %></div>
|
||||
<div class="wizard-body flex vertical justified">
|
||||
<table class="progressTable">
|
||||
<tr class="js-group">
|
||||
<tr class="js-groups">
|
||||
<td><span class="js-done">-</span>/<span class="js-total">-</span>
|
||||
<td><span><%- @T('Groups') %></span>
|
||||
<td class="progressTable-progressCell">
|
||||
|
@ -73,7 +73,7 @@
|
|||
<%- @Icon('checkmark') %>
|
||||
</div>
|
||||
</tr>
|
||||
<tr class="js-organization">
|
||||
<tr class="js-organizations">
|
||||
<td><span class="js-done">-</span>/<span class="js-total">-</span>
|
||||
<td><span><%- @T('Organizations') %></span>
|
||||
<td class="progressTable-progressCell">
|
||||
|
@ -82,7 +82,7 @@
|
|||
<%- @Icon('checkmark') %>
|
||||
</div>
|
||||
</tr>
|
||||
<tr class="js-user">
|
||||
<tr class="js-users">
|
||||
<td><span class="js-done">-</span>/<span class="js-total">-</span>
|
||||
<td><span><%- @T('Users') %></span>
|
||||
<td class="progressTable-progressCell">
|
||||
|
@ -91,7 +91,7 @@
|
|||
<%- @Icon('checkmark') %>
|
||||
</div>
|
||||
</tr>
|
||||
<tr class="js-ticket">
|
||||
<tr class="js-tickets">
|
||||
<td><span class="js-done">-</span>/<span class="js-total">-</span>
|
||||
<td><span><%- @T('Tickets') %></span>
|
||||
<td class="progressTable-progressCell">
|
||||
|
|
|
@ -20,18 +20,18 @@
|
|||
<% if @job.result && @job.result.error: %>
|
||||
<p><%- @Ttimestamp(@job.started_at) %></p>
|
||||
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
|
||||
<% else if !@countDone: %>
|
||||
<% else if @job.result && !@job.result.sum: %>
|
||||
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Counting entries. This may take a while.') %></p>
|
||||
<% else: %>
|
||||
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p>
|
||||
<div class="flex">
|
||||
<progress max="<%= @job.result.sum %>" value="<%= @countDone %>"></progress>
|
||||
<progress max="<%= @job.result.total %>" value="<%= @job.result.sum %>"></progress>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if !_.isEmpty(@job.result) && @countDone: %>
|
||||
<% if !_.isEmpty(@job.result) && @job.result.sum: %>
|
||||
<ul>
|
||||
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
|
||||
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @job.result.sum %>/<%= @job.result.total %>):
|
||||
<ul>
|
||||
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
|
||||
</ul>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ul>
|
||||
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @countDone %>):
|
||||
<li><%- @T('%s user to %s user', 'Exchange', 'Zammad') %> (<%= @job.result.total %>):
|
||||
<ul>
|
||||
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>
|
||||
</ul>
|
||||
|
|
|
@ -5,19 +5,17 @@
|
|||
<table class="table">
|
||||
<thead>
|
||||
<th style="width: 30px"></th>
|
||||
<th style="width: 50px"><%- @T('ID') %></th>
|
||||
<th style="width: 100px"><%- @T('ID') %></th>
|
||||
<th><%- @T('Name') %></th>
|
||||
<th><%- @T('Status') %></th>
|
||||
<th><%- @T('Link') %></th>
|
||||
<th style="width: 100px;"><%- @T('Status') %></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for item in @items: %>
|
||||
<tr>
|
||||
<td><input type="checkbox" name="object_id" value="<%= item.id %>"/></td>
|
||||
<td><%= item.id %></td>
|
||||
<td><%= item.title %></td>
|
||||
<td title="<%= item.id %>"><%= item.id %></td>
|
||||
<td title="<%= item.title %>"><a href="<%- item.link %>" target="_blank"><%= item.title %></a></td>
|
||||
<td><%= item.cmdb_status_title %></td>
|
||||
<td><a href="<%- item.link %>" target="_blank">i-doit</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
|
|
|
@ -20,18 +20,18 @@
|
|||
<% if @job.result && @job.result.error: %>
|
||||
<p><%- @Ttimestamp(@job.started_at) %></p>
|
||||
<div class="alert alert--danger" role="alert"><%- @T('An error occurred: %s', @job.result.error) %></div>
|
||||
<% else if !@countDone: %>
|
||||
<% else if !@job.result.sum: %>
|
||||
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Counting entries. This may take a while.') %></p>
|
||||
<% else: %>
|
||||
<p><%- @Ttimestamp(@job.started_at) %> - <%- @T('Running...') %></p>
|
||||
<div class="flex">
|
||||
<progress max="<%= @job.result.sum %>" value="<%= @countDone %>"></progress>
|
||||
<progress max="<%= @job.result.total %>" value="<%= @job.result.sum %>"></progress>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if !_.isEmpty(@job.result) && @countDone: %>
|
||||
<% if !_.isEmpty(@job.result) && @job.result.sum: %>
|
||||
<ul>
|
||||
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>/<%= @job.result.sum %>):
|
||||
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @job.result.sum %>/<%= @job.result.total %>):
|
||||
<ul>
|
||||
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
|
||||
</ul>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<ul>
|
||||
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @countDone %>):
|
||||
<li><%- @T('%s user to %s user', 'LDAP', 'Zammad') %> (<%= @job.result.sum %>):
|
||||
<ul>
|
||||
<li><%- @T('Users') %>: <%= @job.result.created %> <%- @T('created') %>, <%= @job.result.updated %> <%- @T('updated') %>, <%= @job.result.unchanged %> <%- @T('untouched') %>, <%= @job.result.skipped %> <%- @T('skipped') %>, <%= @job.result.failed %> <%- @T('failed') %>, <%= @job.result.deactivated %> <%- @T('deactivated') %>
|
||||
</ul>
|
||||
|
|
|
@ -440,7 +440,7 @@
|
|||
</div>
|
||||
<div class="textBubble-footer">
|
||||
<div class="textBubble-signatur"><span class="js-signature">/je</span></div>
|
||||
<div class="textBubble-letterCount js-letterCount">140</div>
|
||||
<div class="textBubble-letterCount js-letterCount">280</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue