Merge branch 'develop' of git.znuny.com:zammad/zammad into develop
This commit is contained in:
commit
165a46450f
77 changed files with 2616 additions and 683 deletions
|
@ -127,6 +127,17 @@ test:integration:email_deliver:
|
||||||
- ruby -I test/ test/integration/email_deliver_test.rb
|
- ruby -I test/ test/integration/email_deliver_test.rb
|
||||||
- rake db:drop
|
- rake db:drop
|
||||||
|
|
||||||
|
test:integration:email_keep_on_server:
|
||||||
|
stage: test
|
||||||
|
tags:
|
||||||
|
- core
|
||||||
|
script:
|
||||||
|
- export RAILS_ENV=test
|
||||||
|
- rake db:create
|
||||||
|
- rake db:migrate
|
||||||
|
- ruby -I test/ test/integration/email_keep_on_server_test.rb
|
||||||
|
- rake db:drop
|
||||||
|
|
||||||
test:integration:twitter:
|
test:integration:twitter:
|
||||||
stage: test
|
stage: test
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -7,7 +7,6 @@ notifications:
|
||||||
env:
|
env:
|
||||||
- DB=mysql
|
- DB=mysql
|
||||||
- DB=postgresql
|
- DB=postgresql
|
||||||
- BUNDLE_JOBS=8
|
|
||||||
addons:
|
addons:
|
||||||
postgresql: "9.4"
|
postgresql: "9.4"
|
||||||
apt:
|
apt:
|
||||||
|
|
1
Gemfile
1
Gemfile
|
@ -40,6 +40,7 @@ gem 'omniauth-gitlab'
|
||||||
gem 'omniauth-google-oauth2'
|
gem 'omniauth-google-oauth2'
|
||||||
gem 'omniauth-linkedin-oauth2'
|
gem 'omniauth-linkedin-oauth2'
|
||||||
gem 'omniauth-twitter'
|
gem 'omniauth-twitter'
|
||||||
|
gem 'omniauth-microsoft-office365'
|
||||||
|
|
||||||
gem 'twitter'
|
gem 'twitter'
|
||||||
gem 'telegramAPI'
|
gem 'telegramAPI'
|
||||||
|
|
35
Gemfile.lock
35
Gemfile.lock
|
@ -151,10 +151,11 @@ GEM
|
||||||
guard (~> 2.8)
|
guard (~> 2.8)
|
||||||
guard-compat (~> 1.0)
|
guard-compat (~> 1.0)
|
||||||
multi_json (~> 1.8)
|
multi_json (~> 1.8)
|
||||||
guard-symlink (0.1.0)
|
guard-symlink (0.1.1)
|
||||||
|
guard
|
||||||
guard-compat (~> 1.1)
|
guard-compat (~> 1.1)
|
||||||
hashdiff (0.3.2)
|
hashdiff (0.3.2)
|
||||||
hashie (3.4.4)
|
hashie (3.5.5)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
http (1.0.4)
|
http (1.0.4)
|
||||||
addressable (~> 2.3)
|
addressable (~> 2.3)
|
||||||
|
@ -169,7 +170,7 @@ GEM
|
||||||
icalendar (2.4.1)
|
icalendar (2.4.1)
|
||||||
inflection (1.0.0)
|
inflection (1.0.0)
|
||||||
json (1.8.6)
|
json (1.8.6)
|
||||||
jwt (1.5.4)
|
jwt (1.5.6)
|
||||||
kgio (2.11.0)
|
kgio (2.11.0)
|
||||||
koala (2.4.0)
|
koala (2.4.0)
|
||||||
addressable
|
addressable
|
||||||
|
@ -194,7 +195,7 @@ GEM
|
||||||
mini_portile2 (2.2.0)
|
mini_portile2 (2.2.0)
|
||||||
minitest (5.10.2)
|
minitest (5.10.2)
|
||||||
multi_json (1.12.1)
|
multi_json (1.12.1)
|
||||||
multi_xml (0.5.5)
|
multi_xml (0.6.0)
|
||||||
multipart-post (2.0.0)
|
multipart-post (2.0.0)
|
||||||
mysql2 (0.4.6)
|
mysql2 (0.4.6)
|
||||||
naught (1.1.0)
|
naught (1.1.0)
|
||||||
|
@ -208,33 +209,36 @@ GEM
|
||||||
nenv (~> 0.1)
|
nenv (~> 0.1)
|
||||||
shellany (~> 0.0)
|
shellany (~> 0.0)
|
||||||
oauth (0.5.1)
|
oauth (0.5.1)
|
||||||
oauth2 (1.2.0)
|
oauth2 (1.4.0)
|
||||||
faraday (>= 0.8, < 0.10)
|
faraday (>= 0.8, < 0.13)
|
||||||
jwt (~> 1.0)
|
jwt (~> 1.0)
|
||||||
multi_json (~> 1.3)
|
multi_json (~> 1.3)
|
||||||
multi_xml (~> 0.5)
|
multi_xml (~> 0.5)
|
||||||
rack (>= 1.2, < 3)
|
rack (>= 1.2, < 3)
|
||||||
octokit (4.4.1)
|
octokit (4.4.1)
|
||||||
sawyer (~> 0.7.0, >= 0.5.3)
|
sawyer (~> 0.7.0, >= 0.5.3)
|
||||||
omniauth (1.3.1)
|
omniauth (1.6.1)
|
||||||
hashie (>= 1.2, < 4)
|
hashie (>= 3.4.6, < 3.6.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.6.2, < 3)
|
||||||
omniauth-facebook (4.0.0)
|
omniauth-facebook (4.0.0)
|
||||||
omniauth-oauth2 (~> 1.2)
|
omniauth-oauth2 (~> 1.2)
|
||||||
omniauth-github (1.1.2)
|
omniauth-github (1.3.0)
|
||||||
omniauth (~> 1.0)
|
omniauth (~> 1.5)
|
||||||
omniauth-oauth2 (~> 1.1)
|
omniauth-oauth2 (>= 1.4.0, < 2.0)
|
||||||
omniauth-gitlab (1.0.2)
|
omniauth-gitlab (1.0.2)
|
||||||
omniauth (~> 1.0)
|
omniauth (~> 1.0)
|
||||||
omniauth-oauth2 (~> 1.0)
|
omniauth-oauth2 (~> 1.0)
|
||||||
omniauth-google-oauth2 (0.4.1)
|
omniauth-google-oauth2 (0.5.0)
|
||||||
jwt (~> 1.5.2)
|
jwt (~> 1.5)
|
||||||
multi_json (~> 1.3)
|
multi_json (~> 1.3)
|
||||||
omniauth (>= 1.1.1)
|
omniauth (>= 1.1.1)
|
||||||
omniauth-oauth2 (>= 1.3.1)
|
omniauth-oauth2 (>= 1.3.1)
|
||||||
omniauth-linkedin-oauth2 (0.1.5)
|
omniauth-linkedin-oauth2 (0.1.5)
|
||||||
omniauth (~> 1.0)
|
omniauth (~> 1.0)
|
||||||
omniauth-oauth2
|
omniauth-oauth2
|
||||||
|
omniauth-microsoft-office365 (0.0.7)
|
||||||
|
omniauth
|
||||||
|
omniauth-oauth2
|
||||||
omniauth-oauth (1.1.0)
|
omniauth-oauth (1.1.0)
|
||||||
oauth
|
oauth
|
||||||
omniauth (~> 1.0)
|
omniauth (~> 1.0)
|
||||||
|
@ -463,6 +467,7 @@ DEPENDENCIES
|
||||||
omniauth-gitlab
|
omniauth-gitlab
|
||||||
omniauth-google-oauth2
|
omniauth-google-oauth2
|
||||||
omniauth-linkedin-oauth2
|
omniauth-linkedin-oauth2
|
||||||
|
omniauth-microsoft-office365
|
||||||
omniauth-oauth2
|
omniauth-oauth2
|
||||||
omniauth-twitter
|
omniauth-twitter
|
||||||
pg
|
pg
|
||||||
|
@ -499,4 +504,4 @@ RUBY VERSION
|
||||||
ruby 2.3.1p112
|
ruby 2.3.1p112
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
1.13.7
|
1.15.1
|
||||||
|
|
|
@ -560,21 +560,24 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
||||||
|
|
||||||
# inbound
|
# inbound
|
||||||
configureAttributesInbound = [
|
configureAttributesInbound = [
|
||||||
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
||||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
||||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', },
|
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off' },
|
||||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
|
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||||
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
|
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
|
||||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
||||||
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false },
|
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' },
|
||||||
|
{ name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
|
||||||
]
|
]
|
||||||
|
|
||||||
showHideFolder = (params, attribute, attributes, classname, form, ui) ->
|
showHideFolder = (params, attribute, attributes, classname, form, ui) ->
|
||||||
return if !params
|
return if !params
|
||||||
if params.adapter is 'imap'
|
if params.adapter is 'imap'
|
||||||
ui.show('options::folder')
|
ui.show('options::folder')
|
||||||
|
ui.show('options::keep_on_server')
|
||||||
return
|
return
|
||||||
ui.hide('options::folder')
|
ui.hide('options::folder')
|
||||||
|
ui.hide('options::keep_on_server')
|
||||||
|
|
||||||
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
||||||
return if !params
|
return if !params
|
||||||
|
@ -606,9 +609,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
||||||
# fill user / password based on intro info
|
# fill user / password based on intro info
|
||||||
channel_used = { options: {} }
|
channel_used = { options: {} }
|
||||||
if @account['meta']
|
if @account['meta']
|
||||||
channel_used['options']['user'] = @account['meta']['email']
|
channel_used['options']['user'] = @account['meta']['email']
|
||||||
channel_used['options']['password'] = @account['meta']['password']
|
channel_used['options']['password'] = @account['meta']['password']
|
||||||
channel_used['options']['folder'] = @account['meta']['folder']
|
channel_used['options']['folder'] = @account['meta']['folder']
|
||||||
|
channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server']
|
||||||
|
|
||||||
# show used backend
|
# show used backend
|
||||||
@$('.base-outbound-settings').html('')
|
@$('.base-outbound-settings').html('')
|
||||||
|
@ -670,7 +674,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
||||||
for key, value of data.setting
|
for key, value of data.setting
|
||||||
@account[key] = value
|
@account[key] = value
|
||||||
|
|
||||||
if data.content_messages && data.content_messages > 0
|
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||||
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
||||||
|
@ -724,7 +728,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
||||||
# remember account settings
|
# remember account settings
|
||||||
@account.inbound = params
|
@account.inbound = params
|
||||||
|
|
||||||
if data.content_messages && data.content_messages > 0
|
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||||
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
||||||
|
|
|
@ -9,43 +9,7 @@ class Index extends App.ControllerSubContent
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
render: =>
|
render: =>
|
||||||
auth_provider_all = {
|
auth_provider_all = App.Config.get('auth_provider_all')
|
||||||
facebook: {
|
|
||||||
url: '/auth/facebook'
|
|
||||||
name: 'Facebook'
|
|
||||||
config: 'auth_facebook'
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
url: '/auth/twitter'
|
|
||||||
name: 'Twitter'
|
|
||||||
config: 'auth_twitter'
|
|
||||||
},
|
|
||||||
linkedin: {
|
|
||||||
url: '/auth/linkedin'
|
|
||||||
name: 'LinkedIn'
|
|
||||||
config: 'auth_linkedin'
|
|
||||||
},
|
|
||||||
github: {
|
|
||||||
url: '/auth/github'
|
|
||||||
name: 'GitHub'
|
|
||||||
config: 'auth_github'
|
|
||||||
},
|
|
||||||
gitlab: {
|
|
||||||
url: '/auth/gitlab'
|
|
||||||
name: 'GitLab'
|
|
||||||
config: 'auth_gitlab'
|
|
||||||
},
|
|
||||||
google_oauth2: {
|
|
||||||
url: '/auth/google_oauth2'
|
|
||||||
name: 'Google'
|
|
||||||
config: 'auth_google_oauth2'
|
|
||||||
},
|
|
||||||
oauth2: {
|
|
||||||
url: '/auth/oauth2'
|
|
||||||
name: 'OAuth2'
|
|
||||||
config: 'auth_oauth2'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
auth_providers = {}
|
auth_providers = {}
|
||||||
for key, provider of auth_provider_all
|
for key, provider of auth_provider_all
|
||||||
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
|
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
|
||||||
|
@ -90,3 +54,45 @@ class Index extends App.ControllerSubContent
|
||||||
)
|
)
|
||||||
|
|
||||||
App.Config.set('LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index, permission: ['user_preferences.linked_accounts'] }, 'NavBarProfile')
|
App.Config.set('LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index, permission: ['user_preferences.linked_accounts'] }, 'NavBarProfile')
|
||||||
|
App.Config.set('auth_provider_all', {
|
||||||
|
facebook:
|
||||||
|
url: '/auth/facebook'
|
||||||
|
name: 'Facebook'
|
||||||
|
config: 'auth_facebook'
|
||||||
|
class: 'facebook'
|
||||||
|
twitter:
|
||||||
|
url: '/auth/twitter'
|
||||||
|
name: 'Twitter'
|
||||||
|
config: 'auth_twitter'
|
||||||
|
class: 'twitter'
|
||||||
|
linkedin:
|
||||||
|
url: '/auth/linkedin'
|
||||||
|
name: 'LinkedIn'
|
||||||
|
config: 'auth_linkedin'
|
||||||
|
class: 'linkedin'
|
||||||
|
github:
|
||||||
|
url: '/auth/github'
|
||||||
|
name: 'GitHub'
|
||||||
|
config: 'auth_github'
|
||||||
|
class: 'github'
|
||||||
|
gitlab:
|
||||||
|
url: '/auth/gitlab'
|
||||||
|
name: 'GitLab'
|
||||||
|
config: 'auth_gitlab'
|
||||||
|
class: 'gitlab'
|
||||||
|
microsoft_office365:
|
||||||
|
url: '/auth/microsoft_office365'
|
||||||
|
name: 'Office 365'
|
||||||
|
config: 'auth_microsoft_office365'
|
||||||
|
class: 'office365'
|
||||||
|
google_oauth2:
|
||||||
|
url: '/auth/google_oauth2'
|
||||||
|
name: 'Google'
|
||||||
|
config: 'auth_google_oauth2'
|
||||||
|
class: 'google'
|
||||||
|
oauth2:
|
||||||
|
url: '/auth/oauth2'
|
||||||
|
name: 'OAuth2'
|
||||||
|
config: 'auth_oauth2'
|
||||||
|
class: 'oauth2'
|
||||||
|
})
|
||||||
|
|
|
@ -82,7 +82,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
||||||
view:
|
view:
|
||||||
shown: true
|
shown: true
|
||||||
invite_customer:
|
invite_customer:
|
||||||
show: false
|
shown: false
|
||||||
required: false
|
required: false
|
||||||
'admin.user':
|
'admin.user':
|
||||||
create:
|
create:
|
||||||
|
@ -94,10 +94,10 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
||||||
view:
|
view:
|
||||||
shown: true
|
shown: true
|
||||||
invite_agent:
|
invite_agent:
|
||||||
show: false
|
shown: false
|
||||||
required: false
|
required: false
|
||||||
invite_customer:
|
invite_customer:
|
||||||
show: false
|
shown: false
|
||||||
required: false
|
required: false
|
||||||
Organization:
|
Organization:
|
||||||
'ticket.customer':
|
'ticket.customer':
|
||||||
|
|
|
@ -587,7 +587,7 @@ class ChatWindow extends App.Controller
|
||||||
@sounds.message.play()
|
@sounds.message.play()
|
||||||
@notifyDesktop(
|
@notifyDesktop(
|
||||||
title: @name
|
title: @name
|
||||||
body: message
|
body: App.Utils.html2text(message)
|
||||||
url: '#customer_chat'
|
url: '#customer_chat'
|
||||||
callback: =>
|
callback: =>
|
||||||
App.Event.trigger('chat_focus', { session_id: @session.session_id })
|
App.Event.trigger('chat_focus', { session_id: @session.session_id })
|
||||||
|
|
|
@ -450,8 +450,8 @@ class EmailNotification extends App.WizardFullScreen
|
||||||
if adapter is 'smtp'
|
if adapter is 'smtp'
|
||||||
configureAttributesOutbound = [
|
configureAttributesOutbound = [
|
||||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
|
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
|
||||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password' },
|
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off' },
|
||||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
|
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true },
|
||||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
||||||
]
|
]
|
||||||
@form = new App.ControllerForm(
|
@form = new App.ControllerForm(
|
||||||
|
@ -671,20 +671,24 @@ class ChannelEmail extends App.WizardFullScreen
|
||||||
|
|
||||||
# inbound
|
# inbound
|
||||||
configureAttributesInbound = [
|
configureAttributesInbound = [
|
||||||
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
||||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
||||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', },
|
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', },
|
||||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
|
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', single: true },
|
||||||
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
|
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
|
||||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
||||||
|
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' },
|
||||||
|
{ name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
|
||||||
]
|
]
|
||||||
|
|
||||||
showHideFolder = (params, attribute, attributes, classname, form, ui) ->
|
showHideFolder = (params, attribute, attributes, classname, form, ui) ->
|
||||||
return if !params
|
return if !params
|
||||||
if params.adapter is 'imap'
|
if params.adapter is 'imap'
|
||||||
ui.show('options::folder')
|
ui.show('options::folder')
|
||||||
|
ui.show('options::keep_on_server')
|
||||||
return
|
return
|
||||||
ui.hide('options::folder')
|
ui.hide('options::folder')
|
||||||
|
ui.hide('options::keep_on_server')
|
||||||
|
|
||||||
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
||||||
return if !params
|
return if !params
|
||||||
|
@ -700,7 +704,7 @@ class ChannelEmail extends App.WizardFullScreen
|
||||||
return
|
return
|
||||||
|
|
||||||
new App.ControllerForm(
|
new App.ControllerForm(
|
||||||
el: @$('.base-inbound-settings'),
|
el: @$('.base-inbound-settings')
|
||||||
model:
|
model:
|
||||||
configure_attributes: configureAttributesInbound
|
configure_attributes: configureAttributesInbound
|
||||||
className: ''
|
className: ''
|
||||||
|
@ -716,8 +720,10 @@ class ChannelEmail extends App.WizardFullScreen
|
||||||
# fill user / password based on intro info
|
# fill user / password based on intro info
|
||||||
channel_used = { options: {} }
|
channel_used = { options: {} }
|
||||||
if @account['meta']
|
if @account['meta']
|
||||||
channel_used['options']['user'] = @account['meta']['email']
|
channel_used['options']['user'] = @account['meta']['email']
|
||||||
channel_used['options']['password'] = @account['meta']['password']
|
channel_used['options']['password'] = @account['meta']['password']
|
||||||
|
channel_used['options']['folder'] = @account['meta']['folder']
|
||||||
|
channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server']
|
||||||
|
|
||||||
# show used backend
|
# show used backend
|
||||||
@$('.base-outbound-settings').html('')
|
@$('.base-outbound-settings').html('')
|
||||||
|
@ -725,8 +731,8 @@ class ChannelEmail extends App.WizardFullScreen
|
||||||
if adapter is 'smtp'
|
if adapter is 'smtp'
|
||||||
configureAttributesOutbound = [
|
configureAttributesOutbound = [
|
||||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
|
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
|
||||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', },
|
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', },
|
||||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
|
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true },
|
||||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
||||||
]
|
]
|
||||||
@form = new App.ControllerForm(
|
@form = new App.ControllerForm(
|
||||||
|
@ -745,7 +751,7 @@ class ChannelEmail extends App.WizardFullScreen
|
||||||
@account.meta = params
|
@account.meta = params
|
||||||
|
|
||||||
@disable(e)
|
@disable(e)
|
||||||
@$('.js-probe .js-email').text( params.email )
|
@$('.js-probe .js-email').text(params.email)
|
||||||
@showSlide('js-probe')
|
@showSlide('js-probe')
|
||||||
|
|
||||||
@ajax(
|
@ajax(
|
||||||
|
@ -760,7 +766,7 @@ class ChannelEmail extends App.WizardFullScreen
|
||||||
for key, value of data.setting
|
for key, value of data.setting
|
||||||
@account[key] = value
|
@account[key] = value
|
||||||
|
|
||||||
if data.content_messages && data.content_messages > 0
|
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||||
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
||||||
|
@ -809,7 +815,7 @@ class ChannelEmail extends App.WizardFullScreen
|
||||||
# remember account settings
|
# remember account settings
|
||||||
@account.inbound = params
|
@account.inbound = params
|
||||||
|
|
||||||
if data.content_messages && data.content_messages > 0
|
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||||
message = App.i18n.translateContent('We have already found %s emails in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
message = App.i18n.translateContent('We have already found %s emails in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
||||||
|
|
|
@ -38,50 +38,7 @@ class Index extends App.ControllerContent
|
||||||
)
|
)
|
||||||
|
|
||||||
render: (data = {}) ->
|
render: (data = {}) ->
|
||||||
auth_provider_all = {
|
auth_provider_all = App.Config.get('auth_provider_all')
|
||||||
facebook: {
|
|
||||||
url: '/auth/facebook',
|
|
||||||
name: 'Facebook',
|
|
||||||
config: 'auth_facebook',
|
|
||||||
class: 'facebook'
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
url: '/auth/twitter'
|
|
||||||
name: 'Twitter'
|
|
||||||
config: 'auth_twitter'
|
|
||||||
class: 'twitter'
|
|
||||||
},
|
|
||||||
linkedin: {
|
|
||||||
url: '/auth/linkedin'
|
|
||||||
name: 'LinkedIn'
|
|
||||||
config: 'auth_linkedin'
|
|
||||||
class: 'linkedin'
|
|
||||||
},
|
|
||||||
github: {
|
|
||||||
url: '/auth/github'
|
|
||||||
name: 'GitHub'
|
|
||||||
config: 'auth_github'
|
|
||||||
class: 'github'
|
|
||||||
},
|
|
||||||
gitlab: {
|
|
||||||
url: '/auth/gitlab'
|
|
||||||
name: 'GitLab'
|
|
||||||
config: 'auth_gitlab'
|
|
||||||
class: 'gitlab'
|
|
||||||
},
|
|
||||||
google_oauth2: {
|
|
||||||
url: '/auth/google_oauth2'
|
|
||||||
name: 'Google'
|
|
||||||
config: 'auth_google_oauth2'
|
|
||||||
class: 'google'
|
|
||||||
},
|
|
||||||
oauth2: {
|
|
||||||
url: '/auth/oauth2'
|
|
||||||
name: 'OAuth2'
|
|
||||||
config: 'auth_oauth2'
|
|
||||||
class: 'oauth2'
|
|
||||||
},
|
|
||||||
}
|
|
||||||
auth_providers = []
|
auth_providers = []
|
||||||
for key, provider of auth_provider_all
|
for key, provider of auth_provider_all
|
||||||
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
|
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
|
||||||
|
|
|
@ -391,6 +391,7 @@ class App.TicketZoomArticleActions extends App.Controller
|
||||||
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
|
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
|
||||||
|
|
||||||
# check if quote need to be added
|
# check if quote need to be added
|
||||||
|
signaturePosition = 'bottom'
|
||||||
selected = App.ClipBoard.getSelected('html')
|
selected = App.ClipBoard.getSelected('html')
|
||||||
if selected
|
if selected
|
||||||
selected = App.Utils.htmlCleanup(selected).html()
|
selected = App.Utils.htmlCleanup(selected).html()
|
||||||
|
@ -399,6 +400,16 @@ class App.TicketZoomArticleActions extends App.Controller
|
||||||
if selected
|
if selected
|
||||||
selected = App.Utils.textCleanup(selected)
|
selected = App.Utils.textCleanup(selected)
|
||||||
selected = App.Utils.text2html(selected)
|
selected = App.Utils.text2html(selected)
|
||||||
|
|
||||||
|
# full quote, if needed
|
||||||
|
if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote')
|
||||||
|
signaturePosition = 'top'
|
||||||
|
if article.content_type.match('html')
|
||||||
|
selected = App.Utils.textCleanup(article.body)
|
||||||
|
if article.content_type.match('plain')
|
||||||
|
selected = App.Utils.textCleanup(selected)
|
||||||
|
selected = App.Utils.text2html(selected)
|
||||||
|
|
||||||
if selected
|
if selected
|
||||||
selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>"
|
selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>"
|
||||||
|
|
||||||
|
@ -409,7 +420,12 @@ class App.TicketZoomArticleActions extends App.Controller
|
||||||
|
|
||||||
type = App.TicketArticleType.findByAttribute(name:'email')
|
type = App.TicketArticleType.findByAttribute(name:'email')
|
||||||
|
|
||||||
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
|
App.Event.trigger('ui::ticket::setArticleType', {
|
||||||
|
ticket: @ticket
|
||||||
|
type: type
|
||||||
|
article: articleNew
|
||||||
|
signaturePosition: signaturePosition
|
||||||
|
})
|
||||||
|
|
||||||
telegramPersonalMessageReply: (e) =>
|
telegramPersonalMessageReply: (e) =>
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
|
@ -59,7 +59,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
@$('[name="' + key + '"]').val(value).trigger('change')
|
@$('[name="' + key + '"]').val(value).trigger('change')
|
||||||
|
|
||||||
# preselect article type
|
# preselect article type
|
||||||
@setArticleType(data.type.name)
|
@setArticleType(data.type.name, data.signaturePosition)
|
||||||
|
|
||||||
# set focus at end of field
|
# set focus at end of field
|
||||||
if data.position is 'end'
|
if data.position is 'end'
|
||||||
|
@ -483,7 +483,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
|
|
||||||
@$('[name=internal]').val('')
|
@$('[name=internal]').val('')
|
||||||
|
|
||||||
setArticleType: (type) =>
|
setArticleType: (type, signaturePosition = 'bottom') =>
|
||||||
wasScrolledToBottom = @isScrolledToBottom()
|
wasScrolledToBottom = @isScrolledToBottom()
|
||||||
@type = type
|
@type = type
|
||||||
@$('[name=type]').val(type).trigger('change')
|
@$('[name=type]').val(type).trigger('change')
|
||||||
|
@ -532,7 +532,10 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
body.append('<br><br>')
|
body.append('<br><br>')
|
||||||
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
|
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
|
||||||
App.Utils.htmlStrip(signature)
|
App.Utils.htmlStrip(signature)
|
||||||
body.append(signature)
|
if signaturePosition is 'top'
|
||||||
|
body.prepend(signature)
|
||||||
|
else
|
||||||
|
body.append(signature)
|
||||||
@$('[data-name=body]').replaceWith(body)
|
@$('[data-name=body]').replaceWith(body)
|
||||||
|
|
||||||
# remove old signature
|
# remove old signature
|
||||||
|
@ -566,6 +569,20 @@ class App.TicketZoomArticleNew extends App.Controller
|
||||||
@delay(@updateLetterCount, 600)
|
@delay(@updateLetterCount, 600)
|
||||||
@$('.js-textSizeLimit').removeClass('hide')
|
@$('.js-textSizeLimit').removeClass('hide')
|
||||||
|
|
||||||
|
# convert remote src images to data uri
|
||||||
|
@$('[data-name=body] img').each( (i,image) ->
|
||||||
|
$image = $(image)
|
||||||
|
src = $image.attr('src')
|
||||||
|
if !_.isEmpty(src) && !src.match(/^data:image/i)
|
||||||
|
canvas = document.createElement('canvas')
|
||||||
|
canvas.width = image.width
|
||||||
|
canvas.height = image.height
|
||||||
|
ctx = canvas.getContext('2d')
|
||||||
|
ctx.drawImage(image, 0, 0)
|
||||||
|
dataURL = canvas.toDataURL()
|
||||||
|
$image.attr('src', dataURL)
|
||||||
|
)
|
||||||
|
|
||||||
@scrollToBottom() if wasScrolledToBottom
|
@scrollToBottom() if wasScrolledToBottom
|
||||||
|
|
||||||
isScrolledToBottom: ->
|
isScrolledToBottom: ->
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
class SidebarOrganization extends App.Controller
|
class SidebarOrganization extends App.Controller
|
||||||
sidebarItem: =>
|
sidebarItem: =>
|
||||||
|
return if !@permissionCheck('ticket.agent')
|
||||||
return if !@ticket.organization_id
|
return if !@ticket.organization_id
|
||||||
{
|
{
|
||||||
head: 'Organization'
|
head: 'Organization'
|
||||||
|
|
|
@ -5,10 +5,10 @@ class Widget
|
||||||
banner = """
|
banner = """
|
||||||
|
|
|
|
||||||
| Welcome Zammad Developer!
|
| Welcome Zammad Developer!
|
||||||
| You can enable debugging by the following examples (value is a regex):
|
| You can enable debugging with the following examples (value is a regex):
|
||||||
|
|
|
|
||||||
| App.Log.config('module', '(websocket|delay|interval)') // enable debugging for websocket, delay and interval class
|
| App.Log.config('module', '(websocket|delay|interval)') // enable debugging for websocket, delay and interval class
|
||||||
| App.Log.config('content', 'send') // enable debugging for messages which contains the string 'send'
|
| App.Log.config('content', 'send') // enable debugging for messages which contain the string 'send'
|
||||||
| App.Log.config('banner', false) // disable this banner
|
| App.Log.config('banner', false) // disable this banner
|
||||||
|
|
|
|
||||||
| App.Log.config() // current settings
|
| App.Log.config() // current settings
|
||||||
|
|
|
@ -38,11 +38,16 @@ class App.PrettyDate
|
||||||
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||||
month = months[created.getMonth()]
|
month = months[created.getMonth()]
|
||||||
|
|
||||||
# for less than 7 days
|
# for less than 6 days
|
||||||
if diff < (60 * 60 * 24 * 7)
|
# weekday HH::MM
|
||||||
|
if diff < (60 * 60 * 24 * 6)
|
||||||
string = "#{App.i18n.translateInline(weekday)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
|
string = "#{App.i18n.translateInline(weekday)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
|
||||||
else if diff < (60 * 60 * 24 * 7) * 365
|
# if it was this year
|
||||||
|
# weekday DD. MM HH::MM
|
||||||
|
else if created.getYear() is current.getYear()
|
||||||
string = "#{App.i18n.translateInline(weekday)} #{created.getDate()}. #{App.i18n.translateInline(month)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
|
string = "#{App.i18n.translateInline(weekday)} #{created.getDate()}. #{App.i18n.translateInline(month)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
|
||||||
|
# if it was the year before
|
||||||
|
# weekday YYYY-MM-DD HH::MM
|
||||||
else
|
else
|
||||||
string = "#{App.i18n.translateInline(weekday)} #{App.i18n.translateTimestamp(time)}"
|
string = "#{App.i18n.translateInline(weekday)} #{App.i18n.translateTimestamp(time)}"
|
||||||
if escalation
|
if escalation
|
||||||
|
|
|
@ -295,7 +295,7 @@
|
||||||
else {
|
else {
|
||||||
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
|
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
|
||||||
}
|
}
|
||||||
document.execCommand('insertHTML', false, img)
|
_this.paste(img)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resize if to big
|
// resize if to big
|
||||||
|
@ -367,13 +367,7 @@
|
||||||
text = App.Utils.removeEmptyLines(text)
|
text = App.Utils.removeEmptyLines(text)
|
||||||
_this.log('insert', text)
|
_this.log('insert', text)
|
||||||
|
|
||||||
// as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
|
_this.paste(text)
|
||||||
if (docType == 'text3') {
|
|
||||||
_this.pasteHtmlAtCaret(text)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
document.execCommand('insertHTML', false, text)
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -533,37 +527,6 @@
|
||||||
return this.$element.html().trim()
|
return this.$element.html().trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
// taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
|
|
||||||
Plugin.prototype.pasteHtmlAtCaret = function(html) {
|
|
||||||
var sel, range;
|
|
||||||
if (window.getSelection) {
|
|
||||||
sel = window.getSelection()
|
|
||||||
if (sel.getRangeAt && sel.rangeCount) {
|
|
||||||
range = sel.getRangeAt(0)
|
|
||||||
range.deleteContents()
|
|
||||||
|
|
||||||
var el = document.createElement('div')
|
|
||||||
el.innerHTML = html;
|
|
||||||
var frag = document.createDocumentFragment(), node, lastNode
|
|
||||||
while ( (node = el.firstChild) ) {
|
|
||||||
lastNode = frag.appendChild(node)
|
|
||||||
}
|
|
||||||
range.insertNode(frag)
|
|
||||||
|
|
||||||
if (lastNode) {
|
|
||||||
range = range.cloneRange()
|
|
||||||
range.setStartAfter(lastNode)
|
|
||||||
range.collapse(true)
|
|
||||||
sel.removeAllRanges()
|
|
||||||
sel.addRange(range)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (document.selection && document.selection.type != 'Control') {
|
|
||||||
document.selection.createRange().pasteHTML(html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// log method
|
// log method
|
||||||
Plugin.prototype.log = function() {
|
Plugin.prototype.log = function() {
|
||||||
if (App && App.Log) {
|
if (App && App.Log) {
|
||||||
|
@ -574,7 +537,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$.fn[pluginName] = function ( options ) {
|
// paste some content
|
||||||
|
Plugin.prototype.paste = function(string) {
|
||||||
|
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
||||||
|
|
||||||
|
// IE <= 10
|
||||||
|
if (document.selection && document.selection.createRange) {
|
||||||
|
var range = document.selection.createRange()
|
||||||
|
if (range.pasteHTML) {
|
||||||
|
range.pasteHTML(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// IE == 11
|
||||||
|
else if (isIE11 && document.getSelection) {
|
||||||
|
var range = document.getSelection().getRangeAt(0)
|
||||||
|
var nnode = document.createElement('div')
|
||||||
|
range.surroundContents(nnode)
|
||||||
|
nnode.innerHTML = string
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.execCommand('insertHTML', false, string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$.fn[pluginName] = function (options) {
|
||||||
return this.each(function () {
|
return this.each(function () {
|
||||||
if (!$.data(this, 'plugin_' + pluginName)) {
|
if (!$.data(this, 'plugin_' + pluginName)) {
|
||||||
$.data(this, 'plugin_' + pluginName,
|
$.data(this, 'plugin_' + pluginName,
|
||||||
|
|
|
@ -250,9 +250,21 @@
|
||||||
|
|
||||||
// paste some content
|
// paste some content
|
||||||
Plugin.prototype.paste = function(string) {
|
Plugin.prototype.paste = function(string) {
|
||||||
if (document.selection) { // IE
|
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
||||||
|
|
||||||
|
// IE <= 10
|
||||||
|
if (document.selection && document.selection.createRange) {
|
||||||
var range = document.selection.createRange()
|
var range = document.selection.createRange()
|
||||||
range.pasteHTML(string)
|
if (range.pasteHTML) {
|
||||||
|
range.pasteHTML(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// IE == 11
|
||||||
|
else if (isIE11 && document.getSelection) {
|
||||||
|
var range = document.getSelection().getRangeAt(0)
|
||||||
|
var nnode = document.createElement('div')
|
||||||
|
range.surroundContents(nnode)
|
||||||
|
nnode.innerHTML = string
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
document.execCommand('insertHTML', false, string)
|
document.execCommand('insertHTML', false, string)
|
||||||
|
@ -295,14 +307,7 @@
|
||||||
// for chrome, insert space again
|
// for chrome, insert space again
|
||||||
if (start) {
|
if (start) {
|
||||||
if (spacerChar === ' ') {
|
if (spacerChar === ' ') {
|
||||||
string = " "
|
this.paste(' ')
|
||||||
if (document.selection) { // IE
|
|
||||||
var range = document.selection.createRange()
|
|
||||||
range.pasteHTML(string)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
document.execCommand('insertHTML', false, string)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,10 @@ class App.Model extends Spine.Model
|
||||||
return @title
|
return @title
|
||||||
if @subject
|
if @subject
|
||||||
return @subject
|
return @subject
|
||||||
|
if @phone
|
||||||
|
return @phone
|
||||||
|
if @login
|
||||||
|
return @login
|
||||||
return '???'
|
return '???'
|
||||||
|
|
||||||
displayNameLong: ->
|
displayNameLong: ->
|
||||||
|
@ -57,6 +61,12 @@ class App.Model extends Spine.Model
|
||||||
return @email
|
return @email
|
||||||
if @title
|
if @title
|
||||||
return @title
|
return @title
|
||||||
|
if @subject
|
||||||
|
return @subject
|
||||||
|
if @phone
|
||||||
|
return @phone
|
||||||
|
if @login
|
||||||
|
return @login
|
||||||
return '???'
|
return '???'
|
||||||
|
|
||||||
icon: (user) ->
|
icon: (user) ->
|
||||||
|
@ -165,6 +175,31 @@ class App.Model extends Spine.Model
|
||||||
|
|
||||||
###
|
###
|
||||||
|
|
||||||
|
set new attributes of model (remove already available attributes)
|
||||||
|
|
||||||
|
App.Model.attributesSet(attributes)
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
@attributesSet: (attributes) ->
|
||||||
|
|
||||||
|
configure_attributes = App[ @.className ].configure_attributes
|
||||||
|
attributesNew = []
|
||||||
|
for localAttribute in configure_attributes
|
||||||
|
found = false
|
||||||
|
for attribute in attributes
|
||||||
|
if attribute.name is localAttribute.name
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
if !found
|
||||||
|
attributesNew.push localAttribute
|
||||||
|
for attribute in attributes
|
||||||
|
App[@.className].attributes.push attribute.name
|
||||||
|
attributesNew.push attribute
|
||||||
|
App[ @.className ].configure_attributes = attributesNew
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
attributes = App.Model.attributesGet(optionalScreen, optionalAttributesList)
|
attributes = App.Model.attributesGet(optionalScreen, optionalAttributesList)
|
||||||
|
|
||||||
returns
|
returns
|
||||||
|
|
|
@ -63,7 +63,7 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form class="setup wizard hide js-inbound">
|
<form class="setup wizard hide js-inbound">
|
||||||
<div class="wizard-slide">
|
<div class="wizard-slide wizard-slide--large">
|
||||||
<h2><%- @T('Email Inbound') %></h2>
|
<h2><%- @T('Email Inbound') %></h2>
|
||||||
<div class="wizard-body vertical justified">
|
<div class="wizard-body vertical justified">
|
||||||
<div class="alert alert--danger hide" role="alert"></div>
|
<div class="alert alert--danger hide" role="alert"></div>
|
||||||
|
|
|
@ -51,6 +51,10 @@
|
||||||
<label for="id1">Name</label>
|
<label for="id1">Name</label>
|
||||||
<input id="id1" class="form-control" type="text" placeholder="Text Input">
|
<input id="id1" class="form-control" type="text" placeholder="Text Input">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="input form-group">
|
||||||
|
<label for="id1">Name (readonly)</label>
|
||||||
|
<input id="id1" class="form-control" type="text" placeholder="Text Input" readonly value="Sandor Clegane">
|
||||||
|
</div>
|
||||||
<div class="input form-group">
|
<div class="input form-group">
|
||||||
<label for="id2">Password</label>
|
<label for="id2">Password</label>
|
||||||
<input id="id2" class="form-control" type="password" value="Password Input">
|
<input id="id2" class="form-control" type="password" value="Password Input">
|
||||||
|
|
|
@ -116,8 +116,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="searchfield">
|
<div class="searchfield">
|
||||||
<%- @Icon('magnifier') %>
|
|
||||||
<input class="js-search form-control" name="search" placeholder="Search for users" type="search">
|
<input class="js-search form-control" name="search" placeholder="Search for users" type="search">
|
||||||
|
<%- @Icon('magnifier') %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="userSearch horizontal">
|
<div class="userSearch horizontal">
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<div class="formGroup-label">
|
<div class="formGroup-label">
|
||||||
<label for="password"><%- @Ti('Password') %></label>
|
<label for="password"><%- @Ti('Password') %></label>
|
||||||
</div>
|
</div>
|
||||||
<input id="password" name="password" type="password" class="form-control"/>
|
<input id="password" name="password" type="password" class="form-control" autocomplete="off"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
<div class="detail-search">
|
<div class="detail-search">
|
||||||
<div class="detail-search-header">
|
<div class="detail-search-header">
|
||||||
<div class="searchfield">
|
<div class="searchfield">
|
||||||
<%- @Icon('magnifier') %>
|
|
||||||
<input class="js-search form-control<%= if !@query then ' is-empty' %>" name="query" placeholder="<%- @Ti('Find what you search. E. g. "search phrase"') %>" value="<%= @query %>" type="search" autocomplete="off">
|
<input class="js-search form-control<%= if !@query then ' is-empty' %>" name="query" placeholder="<%- @Ti('Find what you search. E. g. "search phrase"') %>" value="<%= @query %>" type="search" autocomplete="off">
|
||||||
|
<%- @Icon('magnifier') %>
|
||||||
<div class="empty-search js-emptySearch">
|
<div class="empty-search js-emptySearch">
|
||||||
<%- @Icon('diagonal-cross') %>
|
<%- @Icon('diagonal-cross') %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<div class="ticketZoom">
|
<div class="ticketZoom">
|
||||||
<div class="ticketZoom-controls">
|
<div class="ticketZoom-controls">
|
||||||
<div class="js-settingContainer"></div>
|
<div class="js-settingContainer"></div>
|
||||||
|
<div class="spacer"></div>
|
||||||
<div class="js-highlighterContainer highlighter"></div>
|
<div class="js-highlighterContainer highlighter"></div>
|
||||||
<div class="js-overviewNavigatorContainer overview-navigator"></div>
|
<div class="js-overviewNavigatorContainer overview-navigator"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<div class="btn btn--action btn--split--first js-setting centered">
|
<div class="btn btn--action js-setting centered">
|
||||||
<%- @Icon('cog', 'dropdown-icon') %>
|
<%- @Icon('cog', 'dropdown-icon') %>
|
||||||
</div>
|
</div>
|
|
@ -12,8 +12,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<div class="searchfield">
|
<div class="searchfield">
|
||||||
<%- @Icon('magnifier') %>
|
|
||||||
<input class="js-search form-control" name="search" placeholder="<%- @Ti('Search for users') %>" type="search">
|
<input class="js-search form-control" name="search" placeholder="<%- @Ti('Search for users') %>" type="search">
|
||||||
|
<%- @Icon('magnifier') %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="userSearch horizontal">
|
<div class="userSearch horizontal">
|
||||||
|
|
|
@ -55,6 +55,7 @@
|
||||||
.icon-mute { width: 16px; height: 16px; }
|
.icon-mute { width: 16px; height: 16px; }
|
||||||
.icon-note { width: 16px; height: 16px; }
|
.icon-note { width: 16px; height: 16px; }
|
||||||
.icon-oauth2-button { width: 29px; height: 24px; }
|
.icon-oauth2-button { width: 29px; height: 24px; }
|
||||||
|
.icon-office365-button { width: 29px; height: 24px; }
|
||||||
.icon-one-ticket { width: 48px; height: 10px; }
|
.icon-one-ticket { width: 48px; height: 10px; }
|
||||||
.icon-organization { width: 16px; height: 16px; }
|
.icon-organization { width: 16px; height: 16px; }
|
||||||
.icon-outbound-calls { width: 17px; height: 17px; }
|
.icon-outbound-calls { width: 17px; height: 17px; }
|
||||||
|
|
|
@ -398,7 +398,8 @@ pre code.hljs {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-disabled,
|
&.is-disabled,
|
||||||
&[disabled] {
|
&[disabled],
|
||||||
|
&:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: .33;
|
opacity: .33;
|
||||||
|
@ -415,7 +416,7 @@ pre code.hljs {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
height: 31px;
|
height: 31px;
|
||||||
padding: 2px 11px 0 !important;
|
padding: 0 11px !important;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@ -1623,6 +1624,24 @@ textarea,
|
||||||
border-color: hsl(200,71%,59%);
|
border-color: hsl(200,71%,59%);
|
||||||
box-shadow: 0 0 0 3px hsl(201,62%,90%);
|
box-shadow: 0 0 0 3px hsl(201,62%,90%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-disabled, // .is-disabled should not be used - legacy support
|
||||||
|
&[disabled],
|
||||||
|
&[readonly] {
|
||||||
|
background: hsl(210,17%,93%);
|
||||||
|
border-color: hsl(210,10%,85%);
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&.focus {
|
||||||
|
border-color: hsl(200,71%,59%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled, // .is-disabled should not be used
|
||||||
|
&[disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=url] {
|
input[type=url] {
|
||||||
|
@ -1700,13 +1719,6 @@ select.form-control:not([multiple]) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control[disabled], .form-control.is-disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
background-color: #fff;
|
|
||||||
color: #d5d5d5;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control.form-control--borderless {
|
.form-control.form-control--borderless {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -1860,6 +1872,7 @@ input.has-error {
|
||||||
appearance: textfield;
|
appearance: textfield;
|
||||||
border-radius: 19px;
|
border-radius: 19px;
|
||||||
padding: 0 17px 0 42px;
|
padding: 0 17px 0 42px;
|
||||||
|
will-change: transform;
|
||||||
|
|
||||||
&.is-empty + .empty-search {
|
&.is-empty + .empty-search {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -2550,6 +2563,10 @@ ol.tabs li {
|
||||||
background: hsl(0,0%,15%);
|
background: hsl(0,0%,15%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.auth-provider--office365 {
|
||||||
|
background: hsl(15,100%,47%);
|
||||||
|
}
|
||||||
|
|
||||||
.provider-name {
|
.provider-name {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -2558,7 +2575,6 @@ ol.tabs li {
|
||||||
width: 29px;
|
width: 29px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
margin-top: 1px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3956,7 +3972,6 @@ footer {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@extend .zIndex-2;
|
|
||||||
|
|
||||||
&.is-visible {
|
&.is-visible {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -5483,8 +5498,13 @@ footer {
|
||||||
.newTicket .sidebar {
|
.newTicket .sidebar {
|
||||||
width: 290px;
|
width: 290px;
|
||||||
}
|
}
|
||||||
.newTicket .form-control:not(:focus):not(.focus) {
|
.newTicket .form-control {
|
||||||
border-color: hsl(0,0%,90%);
|
border-color: hsl(0,0%,90%);
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&.focus {
|
||||||
|
border-color: hsl(200,71%,59%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.newTicket .article-form-top {
|
.newTicket .article-form-top {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
|
@ -5964,6 +5984,8 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
|
@extend .zIndex-5; // has to be behind modal windows and beneath notifications (popover)
|
||||||
|
position: absolute;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
@ -6303,15 +6325,18 @@ footer {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox.form-group .controls label {
|
.checkbox,
|
||||||
padding: 2px 0;
|
.radio {
|
||||||
font: inherit;
|
&.form-group .controls label {
|
||||||
font-size: 13px;
|
padding: 2px 0;
|
||||||
margin-bottom: 0;
|
font: inherit;
|
||||||
color: inherit;
|
font-size: 13px;
|
||||||
text-transform: inherit;
|
margin-bottom: 0;
|
||||||
letter-spacing: 0;
|
color: inherit;
|
||||||
@extend .u-clickable;
|
text-transform: inherit;
|
||||||
|
letter-spacing: 0;
|
||||||
|
@extend .u-clickable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.userSearch-label {
|
.userSearch-label {
|
||||||
|
@ -6440,6 +6465,9 @@ footer {
|
||||||
@extend .u-textTruncate;
|
@extend .u-textTruncate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wizard {
|
||||||
|
margin: auto; // makes sure that the wizard is scrollable
|
||||||
|
}
|
||||||
|
|
||||||
.wizard-logo {
|
.wizard-logo {
|
||||||
fill: white;
|
fill: white;
|
||||||
|
@ -6454,6 +6482,10 @@ footer {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
padding-bottom: 18px;
|
padding-bottom: 18px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&.wizard-slide--large {
|
||||||
|
width: 460px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wizard h2 {
|
.wizard h2 {
|
||||||
|
@ -7178,6 +7210,12 @@ output {
|
||||||
.zammad-switch input {
|
.zammad-switch input {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
&[disabled] + label {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: hsl(210,17%,93%);
|
||||||
|
border-color: hsl(210,10%,85%);
|
||||||
|
}
|
||||||
|
|
||||||
&:focus + label {
|
&:focus + label {
|
||||||
transition: none;
|
transition: none;
|
||||||
background: hsl(200,71%,59%);
|
background: hsl(200,71%,59%);
|
||||||
|
|
|
@ -126,6 +126,16 @@ class UsersController < ApplicationController
|
||||||
if admin_account_exists && !params[:signup]
|
if admin_account_exists && !params[:signup]
|
||||||
raise Exceptions::UnprocessableEntity, 'Only signup with not authenticate user possible!'
|
raise Exceptions::UnprocessableEntity, 'Only signup with not authenticate user possible!'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# check if user already exists
|
||||||
|
if clean_params[:email].blank?
|
||||||
|
raise Exceptions::UnprocessableEntity, 'Attribute \'email\' required!'
|
||||||
|
end
|
||||||
|
|
||||||
|
# check if user already exists
|
||||||
|
exists = User.find_by(email: clean_params[:email].downcase.strip)
|
||||||
|
raise Exceptions::UnprocessableEntity, 'Email address is already used for other user.' if exists
|
||||||
|
|
||||||
user = User.new(clean_params)
|
user = User.new(clean_params)
|
||||||
user.associations_from_param(params)
|
user.associations_from_param(params)
|
||||||
user.updated_by_id = 1
|
user.updated_by_id = 1
|
||||||
|
@ -165,11 +175,6 @@ class UsersController < ApplicationController
|
||||||
user.associations_from_param(params)
|
user.associations_from_param(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
# check if user already exists
|
|
||||||
if !user.email.empty?
|
|
||||||
exists = User.where(email: user.email.downcase).first
|
|
||||||
raise Exceptions::UnprocessableEntity, 'User already exists!' if exists
|
|
||||||
end
|
|
||||||
user.save!
|
user.save!
|
||||||
|
|
||||||
# if first user was added, set system init done
|
# if first user was added, set system init done
|
||||||
|
@ -177,7 +182,7 @@ class UsersController < ApplicationController
|
||||||
Setting.set('system_init_done', true)
|
Setting.set('system_init_done', true)
|
||||||
|
|
||||||
# fetch org logo
|
# fetch org logo
|
||||||
if !user.email.empty?
|
if user.email.present?
|
||||||
Service::Image.organization_suggest(user.email)
|
Service::Image.organization_suggest(user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -252,17 +257,17 @@ class UsersController < ApplicationController
|
||||||
|
|
||||||
# only allow Admin's
|
# only allow Admin's
|
||||||
if current_user.permissions?('admin.user') && (params[:role_ids] || params[:roles])
|
if current_user.permissions?('admin.user') && (params[:role_ids] || params[:roles])
|
||||||
user.associations_from_param({ role_ids: params[:role_ids], roles: params[:roles] })
|
user.associations_from_param(role_ids: params[:role_ids], roles: params[:roles])
|
||||||
end
|
end
|
||||||
|
|
||||||
# only allow Admin's
|
# only allow Admin's
|
||||||
if current_user.permissions?('admin.user') && (params[:group_ids] || params[:groups])
|
if current_user.permissions?('admin.user') && (params[:group_ids] || params[:groups])
|
||||||
user.associations_from_param({ group_ids: params[:group_ids], groups: params[:groups] })
|
user.associations_from_param(group_ids: params[:group_ids], groups: params[:groups])
|
||||||
end
|
end
|
||||||
|
|
||||||
# only allow Admin's and Agent's
|
# only allow Admin's and Agent's
|
||||||
if current_user.permissions?(['admin.user', 'ticket.agent']) && (params[:organization_ids] || params[:organizations])
|
if current_user.permissions?(['admin.user', 'ticket.agent']) && (params[:organization_ids] || params[:organizations])
|
||||||
user.associations_from_param({ organization_ids: params[:organization_ids], organizations: params[:organizations] })
|
user.associations_from_param(organization_ids: params[:organization_ids], organizations: params[:organizations])
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:expand]
|
if params[:expand]
|
||||||
|
@ -363,7 +368,7 @@ class UsersController < ApplicationController
|
||||||
limit: params[:limit],
|
limit: params[:limit],
|
||||||
current_user: current_user,
|
current_user: current_user,
|
||||||
}
|
}
|
||||||
if params[:role_ids] && !params[:role_ids].empty?
|
if params[:role_ids].present?
|
||||||
query_params[:role_ids] = params[:role_ids]
|
query_params[:role_ids] = params[:role_ids]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -449,10 +454,10 @@ class UsersController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
# do query
|
# do query
|
||||||
user_all = if params[:role_ids] && !params[:role_ids].empty?
|
user_all = if params[:role_ids].present?
|
||||||
User.joins(:roles).where( 'roles.id' => params[:role_ids] ).where('users.id != 1').order('users.created_at DESC').limit( params[:limit] || 20 )
|
User.joins(:roles).where('roles.id' => params[:role_ids]).where('users.id != 1').order('users.created_at DESC').limit(params[:limit] || 20)
|
||||||
else
|
else
|
||||||
User.where('id != 1').order('created_at DESC').limit( params[:limit] || 20 )
|
User.where('id != 1').order('created_at DESC').limit(params[:limit] || 20)
|
||||||
end
|
end
|
||||||
|
|
||||||
# build result list
|
# build result list
|
||||||
|
|
|
@ -5,7 +5,7 @@ class ApplicationModel < ActiveRecord::Base
|
||||||
include ApplicationModel::HasCache
|
include ApplicationModel::HasCache
|
||||||
include ApplicationModel::CanLookup
|
include ApplicationModel::CanLookup
|
||||||
include ApplicationModel::CanLookupSearchIndexAttributes
|
include ApplicationModel::CanLookupSearchIndexAttributes
|
||||||
include ApplicationModel::ChecksAttributeLength
|
include ApplicationModel::ChecksAttributeValuesAndLength
|
||||||
include ApplicationModel::CanCleanupParam
|
include ApplicationModel::CanCleanupParam
|
||||||
include ApplicationModel::HasRecentViews
|
include ApplicationModel::HasRecentViews
|
||||||
include ApplicationModel::ChecksUserColumnsFillup
|
include ApplicationModel::ChecksUserColumnsFillup
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
module ApplicationModel::ChecksAttributeLength
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
included do
|
|
||||||
before_create :check_attribute_length
|
|
||||||
before_update :check_attribute_length
|
|
||||||
end
|
|
||||||
|
|
||||||
=begin
|
|
||||||
|
|
||||||
check string/varchar size and cut them if needed
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def check_attribute_length
|
|
||||||
attributes.each { |attribute|
|
|
||||||
next if !self[ attribute[0] ]
|
|
||||||
next if !self[ attribute[0] ].instance_of?(String)
|
|
||||||
next if self[ attribute[0] ].empty?
|
|
||||||
column = self.class.columns_hash[ attribute[0] ]
|
|
||||||
next if !column
|
|
||||||
limit = column.limit
|
|
||||||
if column && limit
|
|
||||||
current_length = attribute[1].to_s.length
|
|
||||||
if limit < current_length
|
|
||||||
logger.warn "WARNING: cut string because of database length #{self.class}.#{attribute[0]}(#{limit} but is #{current_length}:#{attribute[1]})"
|
|
||||||
self[ attribute[0] ] = attribute[1][ 0, limit ]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# strip 4 bytes utf8 chars if needed
|
|
||||||
if column && self[ attribute[0] ]
|
|
||||||
self[attribute[0]] = self[ attribute[0] ].utf8_to_3bytesutf8
|
|
||||||
end
|
|
||||||
}
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
module ApplicationModel::ChecksAttributeValuesAndLength
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
before_create :check_attribute_values_and_length
|
||||||
|
before_update :check_attribute_values_and_length
|
||||||
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
1) check string/varchar size and cut them if needed
|
||||||
|
|
||||||
|
2) check string for null byte \u0000 and remove it
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def check_attribute_values_and_length
|
||||||
|
columns = self.class.columns_hash
|
||||||
|
attributes.each { |name, value|
|
||||||
|
next if value.blank?
|
||||||
|
next if !value.instance_of?(String)
|
||||||
|
column = columns[name]
|
||||||
|
next if !column
|
||||||
|
|
||||||
|
# strip null byte chars (postgresql will complain about it)
|
||||||
|
if column.sql_type == 'text'
|
||||||
|
if Rails.application.config.db_null_byte == false
|
||||||
|
self[name].delete!("\u0000")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# for varchar check length and replace null bytes
|
||||||
|
limit = column.limit
|
||||||
|
if limit
|
||||||
|
current_length = value.length
|
||||||
|
if limit < current_length
|
||||||
|
logger.warn "WARNING: cut string because of database length #{self.class}.#{name}(#{limit} but is #{current_length}:#{value})"
|
||||||
|
self[name] = value[0, limit]
|
||||||
|
end
|
||||||
|
|
||||||
|
# strip null byte chars (postgresql will complain about it)
|
||||||
|
if Rails.application.config.db_null_byte == false
|
||||||
|
self[name].delete!("\u0000")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# strip 4 bytes utf8 chars if needed (mysql/mariadb will complain it)
|
||||||
|
next if self[name].blank?
|
||||||
|
self[name] = self[name].utf8_to_3bytesutf8
|
||||||
|
}
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
|
@ -52,6 +52,7 @@ example
|
||||||
host: 'outlook.office365.com',
|
host: 'outlook.office365.com',
|
||||||
user: 'xxx@znuny.onmicrosoft.com',
|
user: 'xxx@znuny.onmicrosoft.com',
|
||||||
password: 'xxx',
|
password: 'xxx',
|
||||||
|
keep_on_server: true,
|
||||||
}
|
}
|
||||||
channel = Channel.last
|
channel = Channel.last
|
||||||
instance = Channel::Driver::Imap.new
|
instance = Channel::Driver::Imap.new
|
||||||
|
@ -60,13 +61,18 @@ example
|
||||||
=end
|
=end
|
||||||
|
|
||||||
def fetch (options, channel, check_type = '', verify_string = '')
|
def fetch (options, channel, check_type = '', verify_string = '')
|
||||||
ssl = true
|
ssl = true
|
||||||
port = 993
|
port = 993
|
||||||
|
keep_on_server = false
|
||||||
|
folder = 'INBOX'
|
||||||
|
if options[:keep_on_server] == true || options[:keep_on_server] == 'true'
|
||||||
|
keep_on_server = true
|
||||||
|
end
|
||||||
if options.key?(:ssl) && options[:ssl] == false
|
if options.key?(:ssl) && options[:ssl] == false
|
||||||
ssl = false
|
ssl = false
|
||||||
port = 143
|
port = 143
|
||||||
end
|
end
|
||||||
if options.key?(:port) && !options[:port].empty?
|
if options.key?(:port) && options[:port].present?
|
||||||
port = options[:port]
|
port = options[:port]
|
||||||
|
|
||||||
# disable ssl for non ssl ports
|
# disable ssl for non ssl ports
|
||||||
|
@ -74,8 +80,11 @@ example
|
||||||
ssl = false
|
ssl = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if options[:folder].present?
|
||||||
|
folder = options[:folder]
|
||||||
|
end
|
||||||
|
|
||||||
Rails.logger.info "fetching imap (#{options[:host]}/#{options[:user]} port=#{port},ssl=#{ssl},folder=#{options[:folder]})"
|
Rails.logger.info "fetching imap (#{options[:host]}/#{options[:user]} port=#{port},ssl=#{ssl},folder=#{folder},keep_on_server=#{keep_on_server})"
|
||||||
|
|
||||||
# on check, reduce open_timeout to have faster probing
|
# on check, reduce open_timeout to have faster probing
|
||||||
timeout = 45
|
timeout = 45
|
||||||
|
@ -90,17 +99,17 @@ example
|
||||||
@imap.login(options[:user], options[:password])
|
@imap.login(options[:user], options[:password])
|
||||||
|
|
||||||
# select folder
|
# select folder
|
||||||
if !options[:folder] || options[:folder].empty?
|
@imap.select(folder)
|
||||||
@imap.select('INBOX')
|
|
||||||
else
|
|
||||||
@imap.select(options[:folder])
|
|
||||||
end
|
|
||||||
|
|
||||||
# sort messages by date on server (if not supported), if not fetch messages via search (first in, first out)
|
# sort messages by date on server (if not supported), if not fetch messages via search (first in, first out)
|
||||||
|
filter = ['ALL']
|
||||||
|
if keep_on_server && check_type != 'check' && check_type != 'verify'
|
||||||
|
filter = %w(NOT SEEN)
|
||||||
|
end
|
||||||
begin
|
begin
|
||||||
message_ids = @imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
message_ids = @imap.sort(['DATE'], filter, 'US-ASCII')
|
||||||
rescue
|
rescue
|
||||||
message_ids = @imap.search(['ALL'])
|
message_ids = @imap.search(filter)
|
||||||
end
|
end
|
||||||
|
|
||||||
# check mode only
|
# check mode only
|
||||||
|
@ -168,9 +177,8 @@ example
|
||||||
message_ids.each do |message_id|
|
message_ids.each do |message_id|
|
||||||
count += 1
|
count += 1
|
||||||
Rails.logger.info " - message #{count}/#{count_all}"
|
Rails.logger.info " - message #{count}/#{count_all}"
|
||||||
#Rails.logger.info msg.to_s
|
|
||||||
|
|
||||||
message_meta = @imap.fetch(message_id, ['RFC822.SIZE', 'FLAGS', 'INTERNALDATE'])[0]
|
message_meta = @imap.fetch(message_id, ['RFC822.SIZE', 'ENVELOPE', 'FLAGS', 'INTERNALDATE'])[0]
|
||||||
|
|
||||||
# ignore to big messages
|
# ignore to big messages
|
||||||
info = too_big?(message_meta, count, count_all)
|
info = too_big?(message_meta, count, count_all)
|
||||||
|
@ -182,14 +190,23 @@ example
|
||||||
# ignore deleted messages
|
# ignore deleted messages
|
||||||
next if deleted?(message_meta, count, count_all)
|
next if deleted?(message_meta, count, count_all)
|
||||||
|
|
||||||
|
# ignore already imported
|
||||||
|
next if already_imported?(message_id, message_meta, count, count_all, keep_on_server)
|
||||||
|
|
||||||
# delete email from server after article was created
|
# delete email from server after article was created
|
||||||
msg = @imap.fetch(message_id, 'RFC822')[0].attr['RFC822']
|
msg = @imap.fetch(message_id, 'RFC822')[0].attr['RFC822']
|
||||||
next if !msg
|
next if !msg
|
||||||
process(channel, msg, false)
|
process(channel, msg, false)
|
||||||
@imap.store(message_id, '+FLAGS', [:Deleted])
|
if !keep_on_server
|
||||||
|
@imap.store(message_id, '+FLAGS', [:Deleted])
|
||||||
|
else
|
||||||
|
@imap.store(message_id, '+FLAGS', [:Seen])
|
||||||
|
end
|
||||||
count_fetched += 1
|
count_fetched += 1
|
||||||
end
|
end
|
||||||
@imap.expunge()
|
if !keep_on_server
|
||||||
|
@imap.expunge()
|
||||||
|
end
|
||||||
disconnect
|
disconnect
|
||||||
if count.zero?
|
if count.zero?
|
||||||
Rails.logger.info ' - no message'
|
Rails.logger.info ' - no message'
|
||||||
|
@ -209,6 +226,20 @@ example
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def already_imported?(message_id, message_meta, count, count_all, keep_on_server)
|
||||||
|
return false if !keep_on_server
|
||||||
|
return false if !message_meta.attr
|
||||||
|
return false if !message_meta.attr['ENVELOPE']
|
||||||
|
local_message_id = message_meta.attr['ENVELOPE'].message_id
|
||||||
|
return false if local_message_id.blank?
|
||||||
|
local_message_id_md5 = Digest::MD5.hexdigest(local_message_id)
|
||||||
|
article = Ticket::Article.where(message_id_md5: local_message_id_md5).order('created_at DESC, id DESC').limit(1).first
|
||||||
|
return false if !article
|
||||||
|
@imap.store(message_id, '+FLAGS', [:Seen])
|
||||||
|
Rails.logger.info " - ignore message #{count}/#{count_all} - because message message id already imported"
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def deleted?(message_meta, count, count_all)
|
def deleted?(message_meta, count, count_all)
|
||||||
return false if !message_meta.attr['FLAGS'].include?(:Deleted)
|
return false if !message_meta.attr['FLAGS'].include?(:Deleted)
|
||||||
Rails.logger.info " - ignore message #{count}/#{count_all} - because message has already delete flag"
|
Rails.logger.info " - ignore message #{count}/#{count_all} - because message has already delete flag"
|
||||||
|
|
|
@ -156,7 +156,9 @@ Check if string is a complete html document. If not, add head and css styles.
|
||||||
|
|
||||||
return html if html =~ /<html>/i
|
return html if html =~ /<html>/i
|
||||||
|
|
||||||
Rails.configuration.html_email_body.sub('###html###', html)
|
# use block form because variable html could contain backslashes and e. g. '\1' that
|
||||||
|
# must not be handled as back-references for regular expressions
|
||||||
|
Rails.configuration.html_email_body.sub('###html###') { html }
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
|
@ -358,5 +358,23 @@ returns
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
=begin
|
||||||
|
|
||||||
|
cleanup caller logs
|
||||||
|
|
||||||
|
Cti::Log.cleanup
|
||||||
|
|
||||||
|
optional you can put the max oldest chat entries as argument
|
||||||
|
|
||||||
|
Cti::Log.cleanup(12.months)
|
||||||
|
|
||||||
|
=end
|
||||||
|
|
||||||
|
def self.cleanup(diff = 12.months)
|
||||||
|
Cti::Log.where('created_at < ?', Time.zone.now - diff).delete_all
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,24 +28,37 @@ class Overview < ApplicationModel
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_link_on_create
|
def fill_link_on_create
|
||||||
return true if !link.empty?
|
return true if link.present?
|
||||||
self.link = link_name(name)
|
self.link = link_name(name)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_link_on_update
|
def fill_link_on_update
|
||||||
return true if link.empty?
|
|
||||||
return true if !changes['name']
|
return true if !changes['name']
|
||||||
|
return true if changes['link']
|
||||||
self.link = link_name(name)
|
self.link = link_name(name)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_name(name)
|
def link_name(name)
|
||||||
link = name.downcase
|
local_link = name.downcase
|
||||||
link.gsub!(/\s/, '_')
|
local_link = local_link.parameterize('_')
|
||||||
link.gsub!(/[^0-9a-z]/i, '_')
|
local_link.gsub!(/\s/, '_')
|
||||||
link.gsub!(/_+/, '_')
|
local_link.gsub!(/_+/, '_')
|
||||||
link
|
local_link = URI.escape(local_link)
|
||||||
|
if local_link.blank?
|
||||||
|
local_link = id || rand(999)
|
||||||
|
end
|
||||||
|
check = true
|
||||||
|
while check
|
||||||
|
exists = Overview.find_by(link: local_link)
|
||||||
|
if exists && exists.id != id
|
||||||
|
local_link = "#{local_link}_#{rand(999)}"
|
||||||
|
else
|
||||||
|
check = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local_link
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
|
|
||||||
class Signature < ApplicationModel
|
class Signature < ApplicationModel
|
||||||
include ChecksLatestChangeObserved
|
include ChecksLatestChangeObserved
|
||||||
|
include ChecksHtmlSanitized
|
||||||
|
|
||||||
has_many :groups, after_add: :cache_update, after_remove: :cache_update
|
has_many :groups, after_add: :cache_update, after_remove: :cache_update
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
|
sanitized_html :body
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,10 +2,13 @@
|
||||||
|
|
||||||
class TextModule < ApplicationModel
|
class TextModule < ApplicationModel
|
||||||
include ChecksClientNotification
|
include ChecksClientNotification
|
||||||
|
include ChecksHtmlSanitized
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
validates :content, presence: true
|
validates :content, presence: true
|
||||||
|
|
||||||
|
sanitized_html :content
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
||||||
load text modules from online
|
load text modules from online
|
||||||
|
|
|
@ -38,7 +38,7 @@ class User < ApplicationModel
|
||||||
load 'user/search_index.rb'
|
load 'user/search_index.rb'
|
||||||
include User::SearchIndex
|
include User::SearchIndex
|
||||||
|
|
||||||
before_validation :check_name, :check_email, :check_login, :ensure_password
|
before_validation :check_name, :check_email, :check_login, :ensure_uniq_email, :ensure_password, :ensure_roles, :ensure_identifier
|
||||||
before_create :check_preferences_default, :validate_roles, :domain_based_assignment, :set_locale
|
before_create :check_preferences_default, :validate_roles, :domain_based_assignment, :set_locale
|
||||||
before_update :check_preferences_default, :validate_roles, :reset_login_failed
|
before_update :check_preferences_default, :validate_roles, :reset_login_failed
|
||||||
after_create :avatar_for_email_check
|
after_create :avatar_for_email_check
|
||||||
|
@ -845,7 +845,7 @@ returns
|
||||||
|
|
||||||
def check_email
|
def check_email
|
||||||
return true if Setting.get('import_mode')
|
return true if Setting.get('import_mode')
|
||||||
return true if email.empty?
|
return true if email.blank?
|
||||||
self.email = email.downcase.strip
|
self.email = email.downcase.strip
|
||||||
return true if id == 1
|
return true if id == 1
|
||||||
raise Exceptions::UnprocessableEntity, 'Invalid email' if email !~ /@/
|
raise Exceptions::UnprocessableEntity, 'Invalid email' if email !~ /@/
|
||||||
|
@ -867,9 +867,9 @@ returns
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# if no email, complain about missing login
|
# generate auto login
|
||||||
if id != 1 && login.blank?
|
if login.blank?
|
||||||
raise Exceptions::UnprocessableEntity, 'Attribute \'login\' required!'
|
self.login = "auto-#{Time.zone.now.to_i}-#{rand(999_999)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
# check if login already exists
|
# check if login already exists
|
||||||
|
@ -878,7 +878,7 @@ returns
|
||||||
while check
|
while check
|
||||||
exists = User.find_by(login: login)
|
exists = User.find_by(login: login)
|
||||||
if exists && exists.id != id
|
if exists && exists.id != id
|
||||||
self.login = login + rand(999).to_s
|
self.login = "#{login}#{rand(999)}"
|
||||||
else
|
else
|
||||||
check = false
|
check = false
|
||||||
end
|
end
|
||||||
|
@ -886,6 +886,27 @@ returns
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_roles
|
||||||
|
return true if role_ids.present?
|
||||||
|
self.role_ids = Role.signup_role_ids
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_identifier
|
||||||
|
return true if email.present? || firstname.present? || lastname.present? || phone.present?
|
||||||
|
return true if login.present? && !login.start_with?('auto-')
|
||||||
|
raise Exceptions::UnprocessableEntity, 'Minimum one identifier (login, firstname, lastname, phone or email) for user is required.'
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_uniq_email
|
||||||
|
return true if Setting.get('user_email_multiple_use')
|
||||||
|
return true if Setting.get('import_mode')
|
||||||
|
return true if email.blank?
|
||||||
|
return true if !changes
|
||||||
|
return true if !changes['email']
|
||||||
|
return true if !User.find_by(email: email.downcase.strip)
|
||||||
|
raise Exceptions::UnprocessableEntity, 'Email address is already used for other user.'
|
||||||
|
end
|
||||||
|
|
||||||
def validate_roles
|
def validate_roles
|
||||||
return true if !role_ids
|
return true if !role_ids
|
||||||
role_ids.each { |role_id|
|
role_ids.each { |role_id|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# mysql
|
# mysql
|
||||||
if ActiveRecord::Base.connection_config[:adapter] == 'mysql2'
|
if ActiveRecord::Base.connection_config[:adapter] == 'mysql2'
|
||||||
Rails.application.config.db_4bytes_utf8 = false
|
Rails.application.config.db_4bytes_utf8 = false
|
||||||
|
Rails.application.config.db_null_byte = true
|
||||||
|
|
||||||
# mysql version check
|
# mysql version check
|
||||||
# mysql example: "5.7.3"
|
# mysql example: "5.7.3"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
if ActiveRecord::Base.connection_config[:adapter] == 'postgresql'
|
if ActiveRecord::Base.connection_config[:adapter] == 'postgresql'
|
||||||
Rails.application.config.db_case_sensitive = true
|
Rails.application.config.db_case_sensitive = true
|
||||||
Rails.application.config.db_like = 'ILIKE'
|
Rails.application.config.db_like = 'ILIKE'
|
||||||
|
Rails.application.config.db_null_byte = false
|
||||||
|
|
||||||
# postgresql version check
|
# postgresql version check
|
||||||
# example output: "9.5.0"
|
# example output: "9.5.0"
|
||||||
|
|
|
@ -32,9 +32,11 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
||||||
authorize_url: '/oauth/authorize',
|
authorize_url: '/oauth/authorize',
|
||||||
token_url: '/oauth/token'
|
token_url: '/oauth/token'
|
||||||
},
|
},
|
||||||
scope: 'read_user',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# microsoft_office365 database connect
|
||||||
|
provider :microsoft_office365_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database'
|
||||||
|
|
||||||
# oauth2 database connect
|
# oauth2 database connect
|
||||||
provider :oauth2_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
|
provider :oauth2_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
|
||||||
client_options: {
|
client_options: {
|
||||||
|
|
Binary file not shown.
|
@ -33,6 +33,7 @@ server {
|
||||||
proxy_set_header Connection "Upgrade";
|
proxy_set_header Connection "Upgrade";
|
||||||
proxy_set_header CLIENT_IP $remote_addr;
|
proxy_set_header CLIENT_IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 86400;
|
proxy_read_timeout 86400;
|
||||||
proxy_pass http://zammad-websocket;
|
proxy_pass http://zammad-websocket;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +42,7 @@ server {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header CLIENT_IP $remote_addr;
|
proxy_set_header CLIENT_IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 180;
|
proxy_read_timeout 180;
|
||||||
proxy_pass http://zammad;
|
proxy_pass http://zammad;
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,7 @@ server {
|
||||||
proxy_set_header Connection "Upgrade";
|
proxy_set_header Connection "Upgrade";
|
||||||
proxy_set_header CLIENT_IP $remote_addr;
|
proxy_set_header CLIENT_IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 86400;
|
proxy_read_timeout 86400;
|
||||||
proxy_pass http://zammad-websocket;
|
proxy_pass http://zammad-websocket;
|
||||||
}
|
}
|
||||||
|
@ -132,6 +133,7 @@ server {
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header CLIENT_IP $remote_addr;
|
proxy_set_header CLIENT_IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_read_timeout 180;
|
proxy_read_timeout 180;
|
||||||
proxy_pass http://zammad;
|
proxy_pass http://zammad;
|
||||||
|
|
||||||
|
|
63
db/migrate/20170713000001_omniauth_office365_setting.rb
Normal file
63
db/migrate/20170713000001_omniauth_office365_setting.rb
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
class OmniauthOffice365Setting < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Authentication via %s',
|
||||||
|
name: 'auth_microsoft_office365',
|
||||||
|
area: 'Security::ThirdPartyAuthentication',
|
||||||
|
description: 'Enables user authentication via %s. Register your app first at [%s](%s).',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'auth_microsoft_office365',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
controller: 'SettingsAreaSwitch',
|
||||||
|
sub: ['auth_microsoft_office365_credentials'],
|
||||||
|
title_i18n: ['Office 365'],
|
||||||
|
description_i18n: ['Office 365', 'Microsoft Application Registration Portal', 'https://apps.dev.microsoft.com'],
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Office 365 App Credentials',
|
||||||
|
name: 'auth_microsoft_office365_credentials',
|
||||||
|
area: 'Security::ThirdPartyAuthentication::Office365',
|
||||||
|
description: 'Enables user authentication via Office 365.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: 'App ID',
|
||||||
|
null: true,
|
||||||
|
name: 'app_id',
|
||||||
|
tag: 'input',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'App Secret',
|
||||||
|
null: true,
|
||||||
|
name: 'app_secret',
|
||||||
|
tag: 'input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: {},
|
||||||
|
preferences: {
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
class TicketZoomSetting < ActiveRecord::Migration
|
class TicketZoomSetting2 < ActiveRecord::Migration
|
||||||
def up
|
def up
|
||||||
|
|
||||||
# return if it's a new setup
|
# return if it's a new setup
|
||||||
|
@ -67,6 +67,33 @@ class TicketZoomSetting < ActiveRecord::Migration
|
||||||
},
|
},
|
||||||
frontend: true
|
frontend: true
|
||||||
)
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Email - full quote',
|
||||||
|
name: 'ui_ticket_zoom_article_email_full_quote',
|
||||||
|
area: 'UI::TicketZoom',
|
||||||
|
description: 'Enable if you want to quote the full email in your answer. The quoted email will be put at the end of your answer. If you just want to quote a certain phrase, just mark the text and press reply (this feature is always available).',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'ui_ticket_zoom_article_email_full_quote',
|
||||||
|
tag: 'boolean',
|
||||||
|
translate: true,
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
prio: 220,
|
||||||
|
permission: ['admin.ui'],
|
||||||
|
},
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Twitter - tweet initials',
|
title: 'Twitter - tweet initials',
|
||||||
name: 'ui_ticket_zoom_article_twitter_initials',
|
name: 'ui_ticket_zoom_article_twitter_initials',
|
|
@ -0,0 +1,54 @@
|
||||||
|
class ObjectManagerUserEmailOptional < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
|
||||||
|
ObjectManager::Attribute.add(
|
||||||
|
force: true,
|
||||||
|
object: 'User',
|
||||||
|
name: 'email',
|
||||||
|
display: 'Email',
|
||||||
|
data_type: 'input',
|
||||||
|
data_option: {
|
||||||
|
type: 'email',
|
||||||
|
maxlength: 150,
|
||||||
|
null: true,
|
||||||
|
item_class: 'formGroup--halfSize',
|
||||||
|
},
|
||||||
|
editable: false,
|
||||||
|
active: true,
|
||||||
|
screens: {
|
||||||
|
signup: {
|
||||||
|
'-all-' => {
|
||||||
|
null: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invite_agent: {
|
||||||
|
'-all-' => {
|
||||||
|
null: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
invite_customer: {
|
||||||
|
'-all-' => {
|
||||||
|
null: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
'-all-' => {
|
||||||
|
null: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
'-all-' => {
|
||||||
|
shown: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_create: false,
|
||||||
|
to_migrate: false,
|
||||||
|
to_delete: false,
|
||||||
|
position: 400,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
34
db/migrate/20170714000002_user_email_multiple_use.rb
Normal file
34
db/migrate/20170714000002_user_email_multiple_use.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
class UserEmailMultipleUse < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'User email for muliple users',
|
||||||
|
name: 'user_email_multiple_use',
|
||||||
|
area: 'Model::User',
|
||||||
|
description: 'Allow to use email address for muliple users.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'user_email_multiple_use',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
permission: ['admin'],
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
18
db/migrate/20170714000003_cleanup_cti_log.rb
Normal file
18
db/migrate/20170714000003_cleanup_cti_log.rb
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
class CleanupCtiLog < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.find_by(name: 'system_init_done')
|
||||||
|
|
||||||
|
Scheduler.create_if_not_exists(
|
||||||
|
name: 'Cleanup Cti::Log',
|
||||||
|
method: 'Cti::Log.cleanup',
|
||||||
|
period: 1.month,
|
||||||
|
prio: 2,
|
||||||
|
active: true,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -604,7 +604,7 @@ ObjectManager::Attribute.add(
|
||||||
data_option: {
|
data_option: {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
maxlength: 150,
|
maxlength: 150,
|
||||||
null: false,
|
null: true,
|
||||||
item_class: 'formGroup--halfSize',
|
item_class: 'formGroup--halfSize',
|
||||||
},
|
},
|
||||||
editable: false,
|
editable: false,
|
||||||
|
@ -627,7 +627,7 @@ ObjectManager::Attribute.add(
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
'-all-' => {
|
'-all-' => {
|
||||||
null: false,
|
null: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
|
|
|
@ -156,6 +156,15 @@ Scheduler.create_if_not_exists(
|
||||||
updated_by_id: 1,
|
updated_by_id: 1,
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
)
|
)
|
||||||
|
Scheduler.create_if_not_exists(
|
||||||
|
name: 'Cleanup Cti::Log',
|
||||||
|
method: 'Cti::Log.cleanup',
|
||||||
|
period: 1.month,
|
||||||
|
prio: 2,
|
||||||
|
active: true,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
Scheduler.create_if_not_exists(
|
Scheduler.create_if_not_exists(
|
||||||
name: 'Import Jobs',
|
name: 'Import Jobs',
|
||||||
method: 'ImportJob.start_registered',
|
method: 'ImportJob.start_registered',
|
||||||
|
|
|
@ -600,6 +600,33 @@ Setting.create_if_not_exists(
|
||||||
},
|
},
|
||||||
frontend: true
|
frontend: true
|
||||||
)
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Email - full quote',
|
||||||
|
name: 'ui_ticket_zoom_article_email_full_quote',
|
||||||
|
area: 'UI::TicketZoom',
|
||||||
|
description: 'Enable if you want to quote the full email in your answer. The quoted email will be put at the end of your answer. If you just want to quote a certain phrase, just mark the text and press reply (this feature is always available).',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'ui_ticket_zoom_article_email_full_quote',
|
||||||
|
tag: 'boolean',
|
||||||
|
translate: true,
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
prio: 220,
|
||||||
|
permission: ['admin.ui'],
|
||||||
|
},
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Twitter - tweet initials',
|
title: 'Twitter - tweet initials',
|
||||||
name: 'ui_ticket_zoom_article_twitter_initials',
|
name: 'ui_ticket_zoom_article_twitter_initials',
|
||||||
|
@ -678,6 +705,31 @@ Setting.create_if_not_exists(
|
||||||
},
|
},
|
||||||
frontend: true
|
frontend: true
|
||||||
)
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'User email for muliple users',
|
||||||
|
name: 'user_email_multiple_use',
|
||||||
|
area: 'Model::User',
|
||||||
|
description: 'Allow to use email address for muliple users.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'user_email_multiple_use',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
preferences: {
|
||||||
|
permission: ['admin'],
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Authentication via %s',
|
title: 'Authentication via %s',
|
||||||
name: 'auth_ldap',
|
name: 'auth_ldap',
|
||||||
|
@ -1057,6 +1109,63 @@ Setting.create_if_not_exists(
|
||||||
frontend: false
|
frontend: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Authentication via %s',
|
||||||
|
name: 'auth_microsoft_office365',
|
||||||
|
area: 'Security::ThirdPartyAuthentication',
|
||||||
|
description: 'Enables user authentication via %s. Register your app first at [%s](%s).',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: '',
|
||||||
|
null: true,
|
||||||
|
name: 'auth_microsoft_office365',
|
||||||
|
tag: 'boolean',
|
||||||
|
options: {
|
||||||
|
true => 'yes',
|
||||||
|
false => 'no',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
preferences: {
|
||||||
|
controller: 'SettingsAreaSwitch',
|
||||||
|
sub: ['auth_microsoft_office365_credentials'],
|
||||||
|
title_i18n: ['Office 365'],
|
||||||
|
description_i18n: ['Office 365', 'Microsoft Application Registration Portal', 'https://apps.dev.microsoft.com'],
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
state: false,
|
||||||
|
frontend: true
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Office 365 App Credentials',
|
||||||
|
name: 'auth_microsoft_office365_credentials',
|
||||||
|
area: 'Security::ThirdPartyAuthentication::Office365',
|
||||||
|
description: 'Enables user authentication via Office 365.',
|
||||||
|
options: {
|
||||||
|
form: [
|
||||||
|
{
|
||||||
|
display: 'App ID',
|
||||||
|
null: true,
|
||||||
|
name: 'app_id',
|
||||||
|
tag: 'input',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
display: 'App Secret',
|
||||||
|
null: true,
|
||||||
|
name: 'app_secret',
|
||||||
|
tag: 'input',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
state: {},
|
||||||
|
preferences: {
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Authentication via %s',
|
title: 'Authentication via %s',
|
||||||
name: 'auth_oauth2',
|
name: 'auth_oauth2',
|
||||||
|
@ -1116,7 +1225,7 @@ Setting.create_if_not_exists(
|
||||||
null: true,
|
null: true,
|
||||||
name: 'site',
|
name: 'site',
|
||||||
tag: 'input',
|
tag: 'input',
|
||||||
placeholder: 'https://gitlab.YOURDOMAIN.com',
|
placeholder: 'https://oauth.YOURDOMAIN.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
display: 'authorize_url',
|
display: 'authorize_url',
|
||||||
|
|
|
@ -81,7 +81,7 @@ class String
|
||||||
def html2text(string_only = false, strict = false)
|
def html2text(string_only = false, strict = false)
|
||||||
string = "#{self}" # rubocop:disable Style/UnneededInterpolation
|
string = "#{self}" # rubocop:disable Style/UnneededInterpolation
|
||||||
|
|
||||||
# in case of invalid encodeing, strip invalid chars
|
# in case of invalid encoding, strip invalid chars
|
||||||
# see also test/fixtures/mail21.box
|
# see also test/fixtures/mail21.box
|
||||||
# note: string.encode!('UTF-8', 'UTF-8', :invalid => :replace, :replace => '?') was not detecting invalid chars
|
# note: string.encode!('UTF-8', 'UTF-8', :invalid => :replace, :replace => '?') was not detecting invalid chars
|
||||||
if !string.valid_encoding?
|
if !string.valid_encoding?
|
||||||
|
|
|
@ -66,6 +66,7 @@ class Stats::TicketReopen
|
||||||
def self.log(object, o_id, changes, updated_by_id)
|
def self.log(object, o_id, changes, updated_by_id)
|
||||||
return if object != 'Ticket'
|
return if object != 'Ticket'
|
||||||
ticket = Ticket.lookup(id: o_id)
|
ticket = Ticket.lookup(id: o_id)
|
||||||
|
return if !ticket
|
||||||
|
|
||||||
# check if close_at is already set / if not, ticket is not reopend
|
# check if close_at is already set / if not, ticket is not reopend
|
||||||
return if !ticket.close_at
|
return if !ticket.close_at
|
||||||
|
|
|
@ -197,6 +197,23 @@ do($ = window.jQuery, window) ->
|
||||||
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit <strong>%s</strong> geschlossen.'
|
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation mit <strong>%s</strong> geschlossen.'
|
||||||
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.'
|
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Da Sie in den letzten %s Minuten nichts geschrieben haben wurde Ihre Konversation geschlossen.'
|
||||||
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!'
|
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Es tut uns leid, es dauert länger als erwartet, um einen freien Platz zu erhalten. Bitte versuchen Sie es zu einem späteren Zeitpunkt noch einmal oder schicken Sie uns eine E-Mail. Vielen Dank!'
|
||||||
|
'es'
|
||||||
|
'<strong>Chat</strong> with us!': '<strong>Chatee</strong> con nosotros!'
|
||||||
|
'Scroll down to see new messages': 'Haga scroll hacia abajo para ver nuevos mensajes'
|
||||||
|
'Online': 'En linea'
|
||||||
|
'Online': 'En linea'
|
||||||
|
'Offline': 'Desconectado'
|
||||||
|
'Connecting': 'Conectando'
|
||||||
|
'Connection re-established': 'Conexión restablecida'
|
||||||
|
'Today': 'Hoy'
|
||||||
|
'Send': 'Enviar'
|
||||||
|
'Compose your message...': 'Escriba su mensaje...'
|
||||||
|
'All colleagues are busy.': 'Todos los agentes están ocupados.'
|
||||||
|
'You are on waiting list position <strong>%s</strong>.': 'Usted está en la posición <strong>%s</strong> de la lista de espera.'
|
||||||
|
'Start new conversation': 'Iniciar nueva conversación'
|
||||||
|
'Since you didn\'t respond in the last %s minutes your conversation with <strong>%s</strong> got closed.': 'Puesto que usted no respondió en los últimos %s minutos su conversación con <strong>%s</strong> se ha cerrado.'
|
||||||
|
'Since you didn\'t respond in the last %s minutes your conversation got closed.': 'Puesto que usted no respondió en los últimos %s minutos su conversación se ha cerrado.'
|
||||||
|
'We are sorry, it takes longer as expected to get an empty slot. Please try again later or send us an email. Thank you!': 'Lo sentimos, se tarda más tiempo de lo esperado para ser atendido por un agente. Inténtelo de nuevo más tarde o envíenos un correo electrónico. ¡Gracias!'
|
||||||
'fr':
|
'fr':
|
||||||
'<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous!'
|
'<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous!'
|
||||||
'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages'
|
'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages'
|
||||||
|
@ -251,6 +268,11 @@ do($ = window.jQuery, window) ->
|
||||||
sessionId: undefined
|
sessionId: undefined
|
||||||
scrolledToBottom: true
|
scrolledToBottom: true
|
||||||
scrollSnapTolerance: 10
|
scrollSnapTolerance: 10
|
||||||
|
richTextFormatKey:
|
||||||
|
66: true # b
|
||||||
|
73: true # i
|
||||||
|
85: true # u
|
||||||
|
83: true # s
|
||||||
|
|
||||||
T: (string, items...) =>
|
T: (string, items...) =>
|
||||||
if @options.lang && @options.lang isnt 'en'
|
if @options.lang && @options.lang isnt 'en'
|
||||||
|
@ -367,9 +389,211 @@ do($ = window.jQuery, window) ->
|
||||||
@el.find('.zammad-chat-controls').on 'submit', @onSubmit
|
@el.find('.zammad-chat-controls').on 'submit', @onSubmit
|
||||||
@el.find('.zammad-chat-body').on 'scroll', @detectScrolledtoBottom
|
@el.find('.zammad-chat-body').on 'scroll', @detectScrolledtoBottom
|
||||||
@el.find('.zammad-scroll-hint').click @onScrollHintClick
|
@el.find('.zammad-scroll-hint').click @onScrollHintClick
|
||||||
@input.on
|
@input.on(
|
||||||
keydown: @checkForEnter
|
keydown: @checkForEnter
|
||||||
input: @onInput
|
input: @onInput
|
||||||
|
)
|
||||||
|
@input.on('keydown', (e) =>
|
||||||
|
richtTextControl = false
|
||||||
|
if !e.altKey && !e.ctrlKey && e.metaKey
|
||||||
|
richtTextControl = true
|
||||||
|
else if !e.altKey && e.ctrlKey && !e.metaKey
|
||||||
|
richtTextControl = true
|
||||||
|
|
||||||
|
if richtTextControl && @richTextFormatKey[ e.keyCode ]
|
||||||
|
e.preventDefault()
|
||||||
|
if e.keyCode is 66
|
||||||
|
document.execCommand('bold')
|
||||||
|
return true
|
||||||
|
if e.keyCode is 73
|
||||||
|
document.execCommand('italic')
|
||||||
|
return true
|
||||||
|
if e.keyCode is 85
|
||||||
|
document.execCommand('underline')
|
||||||
|
return true
|
||||||
|
if e.keyCode is 83
|
||||||
|
document.execCommand('strikeThrough')
|
||||||
|
return true
|
||||||
|
)
|
||||||
|
@input.on('paste', (e) =>
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
clipboardData
|
||||||
|
if e.clipboardData
|
||||||
|
clipboardData = e.clipboardData
|
||||||
|
else if window.clipboardData
|
||||||
|
clipboardData = window.clipboardData
|
||||||
|
else if e.originalEvent.clipboardData
|
||||||
|
clipboardData = e.originalEvent.clipboardData
|
||||||
|
else
|
||||||
|
throw 'No clipboardData support'
|
||||||
|
|
||||||
|
imageInserted = false
|
||||||
|
if clipboardData && clipboardData.items && clipboardData.items[0]
|
||||||
|
item = clipboardData.items[0]
|
||||||
|
if item.kind == 'file' && (item.type == 'image/png' || item.type == 'image/jpeg')
|
||||||
|
imageFile = item.getAsFile()
|
||||||
|
reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = (e) =>
|
||||||
|
result = e.target.result
|
||||||
|
img = document.createElement('img')
|
||||||
|
img.src = result
|
||||||
|
|
||||||
|
insert = (dataUrl, width, height, isRetina) =>
|
||||||
|
|
||||||
|
# adapt image if we are on retina devices
|
||||||
|
if @isRetina()
|
||||||
|
width = width / 2
|
||||||
|
height = height / 2
|
||||||
|
result = dataUrl
|
||||||
|
img = "<img style=\"width: 100%; max-width: #{width}px;\" src=\"#{result}\">"
|
||||||
|
document.execCommand('insertHTML', false, img)
|
||||||
|
|
||||||
|
# resize if to big
|
||||||
|
@resizeImage(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
|
||||||
|
|
||||||
|
reader.readAsDataURL(imageFile)
|
||||||
|
imageInserted = true
|
||||||
|
|
||||||
|
return if imageInserted
|
||||||
|
|
||||||
|
# check existing + paste text for limit
|
||||||
|
text = undefined
|
||||||
|
docType = undefined
|
||||||
|
try
|
||||||
|
text = clipboardData.getData('text/html')
|
||||||
|
docType = 'html'
|
||||||
|
if !text || text.length is 0
|
||||||
|
docType = 'text'
|
||||||
|
text = clipboardData.getData('text/plain')
|
||||||
|
if !text || text.length is 0
|
||||||
|
docType = 'text2'
|
||||||
|
text = clipboardData.getData('text')
|
||||||
|
catch e
|
||||||
|
console.log('Sorry, can\'t insert markup because browser is not supporting it.')
|
||||||
|
docType = 'text3'
|
||||||
|
text = clipboardData.getData('text')
|
||||||
|
|
||||||
|
if docType is 'text' || docType is 'text2' || docType is 'text3'
|
||||||
|
text = '<div>' + text.replace(/\n/g, '</div><div>') + '</div>'
|
||||||
|
text = text.replace(/<div><\/div>/g, '<div><br></div>')
|
||||||
|
console.log('p', docType, text)
|
||||||
|
if docType is 'html'
|
||||||
|
html = $("<div>#{text}</div>")
|
||||||
|
match = false
|
||||||
|
htmlTmp = text
|
||||||
|
regex = new RegExp('<(/w|w)\:[A-Za-z]')
|
||||||
|
if htmlTmp.match(regex)
|
||||||
|
match = true
|
||||||
|
htmlTmp = htmlTmp.replace(regex, '')
|
||||||
|
regex = new RegExp('<(/o|o)\:[A-Za-z]')
|
||||||
|
if htmlTmp.match(regex)
|
||||||
|
match = true
|
||||||
|
htmlTmp = htmlTmp.replace(regex, '')
|
||||||
|
if match
|
||||||
|
html = @wordFilter(html)
|
||||||
|
#html
|
||||||
|
|
||||||
|
html = $(html)
|
||||||
|
|
||||||
|
html.contents().each( ->
|
||||||
|
if @nodeType == 8
|
||||||
|
$(@).remove()
|
||||||
|
)
|
||||||
|
|
||||||
|
# remove tags, keep content
|
||||||
|
html.find('a, font, small, time, form, label').replaceWith( ->
|
||||||
|
$(@).contents()
|
||||||
|
)
|
||||||
|
|
||||||
|
# replace tags with generic div
|
||||||
|
# New type of the tag
|
||||||
|
replacementTag = 'div';
|
||||||
|
|
||||||
|
# Replace all x tags with the type of replacementTag
|
||||||
|
html.find('textarea').each( ->
|
||||||
|
outer = @outerHTML
|
||||||
|
|
||||||
|
# Replace opening tag
|
||||||
|
regex = new RegExp('<' + @tagName, 'i')
|
||||||
|
newTag = outer.replace(regex, '<' + replacementTag)
|
||||||
|
|
||||||
|
# Replace closing tag
|
||||||
|
regex = new RegExp('</' + @tagName, 'i')
|
||||||
|
newTag = newTag.replace(regex, '</' + replacementTag)
|
||||||
|
|
||||||
|
$(@).replaceWith(newTag)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
@removeAttributes(html)
|
||||||
|
|
||||||
|
text = html.html()
|
||||||
|
|
||||||
|
# as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
|
||||||
|
if docType is 'text3'
|
||||||
|
@pasteHtmlAtCaret(text)
|
||||||
|
else
|
||||||
|
document.execCommand('insertHTML', false, text)
|
||||||
|
true
|
||||||
|
)
|
||||||
|
@input.on('drop', (e) =>
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
dataTransfer
|
||||||
|
if window.dataTransfer # ie
|
||||||
|
dataTransfer = window.dataTransfer
|
||||||
|
else if e.originalEvent.dataTransfer # other browsers
|
||||||
|
dataTransfer = e.originalEvent.dataTransfer
|
||||||
|
else
|
||||||
|
throw 'No clipboardData support'
|
||||||
|
|
||||||
|
x = e.clientX
|
||||||
|
y = e.clientY
|
||||||
|
file = dataTransfer.files[0]
|
||||||
|
|
||||||
|
# look for images
|
||||||
|
if file.type.match('image.*')
|
||||||
|
reader = new FileReader()
|
||||||
|
reader.onload = (e) =>
|
||||||
|
result = e.target.result
|
||||||
|
img = document.createElement('img')
|
||||||
|
img.src = result
|
||||||
|
|
||||||
|
# Insert the image at the carat
|
||||||
|
insert = (dataUrl, width, height, isRetina) =>
|
||||||
|
|
||||||
|
# adapt image if we are on retina devices
|
||||||
|
if @isRetina()
|
||||||
|
width = width / 2
|
||||||
|
height = height / 2
|
||||||
|
|
||||||
|
result = dataUrl
|
||||||
|
img = $("<img style=\"width: 100%; max-width: #{width}px;\" src=\"#{result}\">")
|
||||||
|
img = img.get(0)
|
||||||
|
|
||||||
|
if document.caretPositionFromPoint
|
||||||
|
pos = document.caretPositionFromPoint(x, y)
|
||||||
|
range = document.createRange()
|
||||||
|
range.setStart(pos.offsetNode, pos.offset)
|
||||||
|
range.collapse()
|
||||||
|
range.insertNode(img)
|
||||||
|
else if document.caretRangeFromPoint
|
||||||
|
range = document.caretRangeFromPoint(x, y)
|
||||||
|
range.insertNode(img)
|
||||||
|
else
|
||||||
|
console.log('could not find carat')
|
||||||
|
|
||||||
|
# resize if to big
|
||||||
|
@resizeImage(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert)
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
)
|
||||||
|
|
||||||
$(window).on('beforeunload', =>
|
$(window).on('beforeunload', =>
|
||||||
@onLeaveTemporary()
|
@onLeaveTemporary()
|
||||||
)
|
)
|
||||||
|
@ -471,7 +695,7 @@ do($ = window.jQuery, window) ->
|
||||||
from: if message.created_by_id then 'agent' else 'customer'
|
from: if message.created_by_id then 'agent' else 'customer'
|
||||||
|
|
||||||
if unfinishedMessage
|
if unfinishedMessage
|
||||||
@input.val unfinishedMessage
|
@input.html(unfinishedMessage)
|
||||||
|
|
||||||
# show wait list
|
# show wait list
|
||||||
if data.position
|
if data.position
|
||||||
|
@ -489,7 +713,7 @@ do($ = window.jQuery, window) ->
|
||||||
@el.find('.zammad-chat-message--unread')
|
@el.find('.zammad-chat-message--unread')
|
||||||
.removeClass 'zammad-chat-message--unread'
|
.removeClass 'zammad-chat-message--unread'
|
||||||
|
|
||||||
sessionStorage.setItem 'unfinished_message', @input.val()
|
sessionStorage.setItem 'unfinished_message', @input.html()
|
||||||
|
|
||||||
@onTyping()
|
@onTyping()
|
||||||
|
|
||||||
|
@ -520,7 +744,7 @@ do($ = window.jQuery, window) ->
|
||||||
@sendMessage()
|
@sendMessage()
|
||||||
|
|
||||||
sendMessage: ->
|
sendMessage: ->
|
||||||
message = @input.val()
|
message = @input.html()
|
||||||
return if !message
|
return if !message
|
||||||
|
|
||||||
@inactiveTimeout.start()
|
@inactiveTimeout.start()
|
||||||
|
@ -543,7 +767,7 @@ do($ = window.jQuery, window) ->
|
||||||
@lastAddedType = 'message--customer'
|
@lastAddedType = 'message--customer'
|
||||||
@el.find('.zammad-chat-body').append messageElement
|
@el.find('.zammad-chat-body').append messageElement
|
||||||
|
|
||||||
@input.val('')
|
@input.html('')
|
||||||
@scrollToBottom()
|
@scrollToBottom()
|
||||||
|
|
||||||
# send message event
|
# send message event
|
||||||
|
@ -585,11 +809,6 @@ do($ = window.jQuery, window) ->
|
||||||
|
|
||||||
@el.addClass('zammad-chat-is-open')
|
@el.addClass('zammad-chat-is-open')
|
||||||
|
|
||||||
if !@inputInitialized
|
|
||||||
@inputInitialized = true
|
|
||||||
@input.autoGrow
|
|
||||||
extraLine: false
|
|
||||||
|
|
||||||
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
|
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
|
||||||
|
|
||||||
@el.css 'bottom', -remainerHeight
|
@el.css 'bottom', -remainerHeight
|
||||||
|
@ -1032,4 +1251,204 @@ do($ = window.jQuery, window) ->
|
||||||
else if direction is 'horizontal'
|
else if direction is 'horizontal'
|
||||||
return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft))
|
return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft))
|
||||||
|
|
||||||
|
isRetina: ->
|
||||||
|
if window.matchMedia
|
||||||
|
mq = window.matchMedia('only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)')
|
||||||
|
return (mq && mq.matches || (window.devicePixelRatio > 1))
|
||||||
|
false
|
||||||
|
|
||||||
|
resizeImage: (dataURL, x = 'auto', y = 'auto', sizeFactor = 1, type, quallity, callback, force = true) ->
|
||||||
|
|
||||||
|
# load image from data url
|
||||||
|
imageObject = new Image()
|
||||||
|
imageObject.onload = ->
|
||||||
|
imageWidth = imageObject.width
|
||||||
|
imageHeight = imageObject.height
|
||||||
|
console.log('ImageService', 'current size', imageWidth, imageHeight)
|
||||||
|
if y is 'auto' && x is 'auto'
|
||||||
|
x = imageWidth
|
||||||
|
y = imageHeight
|
||||||
|
|
||||||
|
# get auto dimensions
|
||||||
|
if y is 'auto'
|
||||||
|
factor = imageWidth / x
|
||||||
|
y = imageHeight / factor
|
||||||
|
|
||||||
|
if x is 'auto'
|
||||||
|
factor = imageWidth / y
|
||||||
|
x = imageHeight / factor
|
||||||
|
|
||||||
|
# check if resize is needed
|
||||||
|
resize = false
|
||||||
|
if x < imageWidth || y < imageHeight
|
||||||
|
resize = true
|
||||||
|
x = x * sizeFactor
|
||||||
|
y = y * sizeFactor
|
||||||
|
else
|
||||||
|
x = imageWidth
|
||||||
|
y = imageHeight
|
||||||
|
|
||||||
|
# create canvas and set dimensions
|
||||||
|
canvas = document.createElement('canvas')
|
||||||
|
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)
|
||||||
|
|
||||||
|
# set quallity based on image size
|
||||||
|
if quallity == 'auto'
|
||||||
|
if x < 200 && y < 200
|
||||||
|
quallity = 1
|
||||||
|
else if x < 400 && y < 400
|
||||||
|
quallity = 0.9
|
||||||
|
else if x < 600 && y < 600
|
||||||
|
quallity = 0.8
|
||||||
|
else if x < 900 && y < 900
|
||||||
|
quallity = 0.7
|
||||||
|
else
|
||||||
|
quallity = 0.6
|
||||||
|
|
||||||
|
# execute callback with resized image
|
||||||
|
newDataUrl = canvas.toDataURL(type, quallity)
|
||||||
|
if resize
|
||||||
|
console.log('ImageService', 'resize', x/sizeFactor, y/sizeFactor, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
|
||||||
|
callback(newDataUrl, x/sizeFactor, y/sizeFactor, true)
|
||||||
|
return
|
||||||
|
console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75)/1024/1024, 'in mb')
|
||||||
|
callback(newDataUrl, x, y, false)
|
||||||
|
|
||||||
|
# load image from data url
|
||||||
|
imageObject.src = dataURL
|
||||||
|
|
||||||
|
# taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
|
||||||
|
pasteHtmlAtCaret: (html) ->
|
||||||
|
sel = undefined
|
||||||
|
range = undefined
|
||||||
|
if window.getSelection
|
||||||
|
sel = window.getSelection()
|
||||||
|
if sel.getRangeAt && sel.rangeCount
|
||||||
|
range = sel.getRangeAt(0)
|
||||||
|
range.deleteContents()
|
||||||
|
|
||||||
|
el = document.createElement('div')
|
||||||
|
el.innerHTML = html
|
||||||
|
frag = document.createDocumentFragment(node, lastNode)
|
||||||
|
while node = el.firstChild
|
||||||
|
lastNode = frag.appendChild(node)
|
||||||
|
range.insertNode(frag)
|
||||||
|
|
||||||
|
if lastNode
|
||||||
|
range = range.cloneRange()
|
||||||
|
range.setStartAfter(lastNode)
|
||||||
|
range.collapse(true)
|
||||||
|
sel.removeAllRanges()
|
||||||
|
sel.addRange(range)
|
||||||
|
else if document.selection && document.selection.type != 'Control'
|
||||||
|
document.selection.createRange().pasteHTML(html)
|
||||||
|
|
||||||
|
# (C) sbrin - https://github.com/sbrin
|
||||||
|
# https://gist.github.com/sbrin/6801034
|
||||||
|
wordFilter: (editor) ->
|
||||||
|
content = editor.html()
|
||||||
|
|
||||||
|
# Word comments like conditional comments etc
|
||||||
|
content = content.replace(/<!--[\s\S]+?-->/gi, '')
|
||||||
|
|
||||||
|
# Remove comments, scripts (e.g., msoShowComment), XML tag, VML content,
|
||||||
|
# MS Office namespaced tags, and a few other tags
|
||||||
|
content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '')
|
||||||
|
|
||||||
|
# Convert <s> into <strike> for line-though
|
||||||
|
content = content.replace(/<(\/?)s>/gi, '<$1strike>')
|
||||||
|
|
||||||
|
# Replace nbsp entites to char since it's easier to handle
|
||||||
|
# content = content.replace(/ /gi, "\u00a0")
|
||||||
|
content = content.replace(/ /gi, ' ')
|
||||||
|
|
||||||
|
# Convert <span style="mso-spacerun:yes">___</span> to string of alternating
|
||||||
|
# breaking/non-breaking spaces of same length
|
||||||
|
#content = content.replace(/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi, (str, spaces) ->
|
||||||
|
# return (spaces.length > 0) ? spaces.replace(/./, " ").slice(Math.floor(spaces.length/2)).split("").join("\u00a0") : ''
|
||||||
|
#)
|
||||||
|
|
||||||
|
editor.html(content)
|
||||||
|
|
||||||
|
# Parse out list indent level for lists
|
||||||
|
$('p', editor).each( ->
|
||||||
|
str = $(@).attr('style')
|
||||||
|
matches = /mso-list:\w+ \w+([0-9]+)/.exec(str)
|
||||||
|
if matches
|
||||||
|
$(@).data('_listLevel', parseInt(matches[1], 10))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parse Lists
|
||||||
|
last_level = 0
|
||||||
|
pnt = null
|
||||||
|
$('p', editor).each(->
|
||||||
|
cur_level = $(@).data('_listLevel')
|
||||||
|
if cur_level != undefined
|
||||||
|
txt = $(@).text()
|
||||||
|
list_tag = '<ul></ul>'
|
||||||
|
if (/^\s*\w+\./.test(txt))
|
||||||
|
matches = /([0-9])\./.exec(txt)
|
||||||
|
if matches
|
||||||
|
start = parseInt(matches[1], 10)
|
||||||
|
list_tag = start>1 ? '<ol start="' + start + '"></ol>' : '<ol></ol>'
|
||||||
|
else
|
||||||
|
list_tag = '<ol></ol>'
|
||||||
|
|
||||||
|
if cur_level > last_level
|
||||||
|
if last_level == 0
|
||||||
|
$(@).before(list_tag)
|
||||||
|
pnt = $(@).prev()
|
||||||
|
else
|
||||||
|
pnt = $(list_tag).appendTo(pnt)
|
||||||
|
|
||||||
|
if cur_level < last_level
|
||||||
|
for i in [i..last_level-cur_level]
|
||||||
|
pnt = pnt.parent()
|
||||||
|
|
||||||
|
$('span:first', @).remove()
|
||||||
|
pnt.append('<li>' + $(@).html() + '</li>')
|
||||||
|
$(@).remove()
|
||||||
|
last_level = cur_level
|
||||||
|
else
|
||||||
|
last_level = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
$('[style]', editor).removeAttr('style')
|
||||||
|
$('[align]', editor).removeAttr('align')
|
||||||
|
$('span', editor).replaceWith(->
|
||||||
|
$(@).contents()
|
||||||
|
)
|
||||||
|
$('span:empty', editor).remove()
|
||||||
|
$("[class^='Mso']", editor).removeAttr('class')
|
||||||
|
$('p:empty', editor).remove()
|
||||||
|
editor
|
||||||
|
|
||||||
|
removeAttribute: (element) ->
|
||||||
|
return if !element
|
||||||
|
$element = $(element)
|
||||||
|
for att in element.attributes
|
||||||
|
if att && att.name
|
||||||
|
element.removeAttribute(att.name)
|
||||||
|
#$element.removeAttr(att.name)
|
||||||
|
|
||||||
|
$element.removeAttr('style')
|
||||||
|
.removeAttr('class')
|
||||||
|
.removeAttr('lang')
|
||||||
|
.removeAttr('type')
|
||||||
|
.removeAttr('align')
|
||||||
|
.removeAttr('id')
|
||||||
|
.removeAttr('wrap')
|
||||||
|
.removeAttr('title')
|
||||||
|
|
||||||
|
removeAttributes: (html, parent = true) =>
|
||||||
|
if parent
|
||||||
|
html.each((index, element) => @removeAttribute(element) )
|
||||||
|
html.find('*').each((index, element) => @removeAttribute(element) )
|
||||||
|
html
|
||||||
|
|
||||||
window.ZammadChat = ZammadChat
|
window.ZammadChat = ZammadChat
|
||||||
|
|
|
@ -320,9 +320,9 @@
|
||||||
.zammad-chat-controls {
|
.zammad-chat-controls {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: none;
|
display: none;
|
||||||
-webkit-align-items: flex-start;
|
-webkit-align-items: flex-end;
|
||||||
-ms-flex-align: start;
|
-ms-flex-align: end;
|
||||||
align-items: flex-start;
|
align-items: flex-end;
|
||||||
border-top: 1px solid #ededed;
|
border-top: 1px solid #ededed;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -340,25 +340,23 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1em 2em;
|
padding: 1em 2em;
|
||||||
float: left;
|
float: left;
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-height: 6em;
|
max-height: 6em;
|
||||||
min-height: 1.4em !important;
|
min-height: 1.4em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: none !important;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
box-shadow: none !important;
|
box-shadow: none;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: none;
|
|
||||||
-webkit-flex: 1;
|
-webkit-flex: 1;
|
||||||
-ms-flex: 1;
|
-ms-flex: 1;
|
||||||
flex: 1; }
|
flex: 1;
|
||||||
|
overflow: auto; }
|
||||||
|
|
||||||
.zammad-chat-input::-webkit-input-placeholder {
|
.zammad-chat-input::-webkit-input-placeholder {
|
||||||
color: #d9d9d9; }
|
color: #d9d9d9; }
|
||||||
|
@ -373,7 +371,7 @@
|
||||||
background: #379ad7;
|
background: #379ad7;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.5em 1.2em;
|
padding: 0.5em 1.2em;
|
||||||
margin: 0.5em 1em 0.5em;
|
margin: 0.63em 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 1.5em;
|
border-radius: 1.5em;
|
||||||
|
|
|
@ -1,3 +1,64 @@
|
||||||
|
if (!window.zammadChatTemplates) {
|
||||||
|
window.zammadChatTemplates = {};
|
||||||
|
}
|
||||||
|
window.zammadChatTemplates["agent"] = function (__obj) {
|
||||||
|
if (!__obj) __obj = {};
|
||||||
|
var __out = [], __capture = function(callback) {
|
||||||
|
var out = __out, result;
|
||||||
|
__out = [];
|
||||||
|
callback.call(this);
|
||||||
|
result = __out.join('');
|
||||||
|
__out = out;
|
||||||
|
return __safe(result);
|
||||||
|
}, __sanitize = function(value) {
|
||||||
|
if (value && value.ecoSafe) {
|
||||||
|
return value;
|
||||||
|
} else if (typeof value !== 'undefined' && value != null) {
|
||||||
|
return __escape(value);
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
|
||||||
|
__safe = __obj.safe = function(value) {
|
||||||
|
if (value && value.ecoSafe) {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
if (!(typeof value !== 'undefined' && value != null)) value = '';
|
||||||
|
var result = new String(value);
|
||||||
|
result.ecoSafe = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!__escape) {
|
||||||
|
__escape = __obj.escape = function(value) {
|
||||||
|
return ('' + value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
(function() {
|
||||||
|
if (this.agent.avatar) {
|
||||||
|
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
|
||||||
|
__out.push(__sanitize(this.agent.avatar));
|
||||||
|
__out.push('">\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
|
||||||
|
|
||||||
|
__out.push(__sanitize(this.agent.name));
|
||||||
|
|
||||||
|
__out.push('</span>\n</span>');
|
||||||
|
|
||||||
|
}).call(this);
|
||||||
|
|
||||||
|
}).call(__obj);
|
||||||
|
__obj.safe = __objSafe, __obj.escape = __escape;
|
||||||
|
return __out.join('');
|
||||||
|
};
|
||||||
|
|
||||||
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
||||||
slice = [].slice,
|
slice = [].slice,
|
||||||
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
|
||||||
|
@ -60,7 +121,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
Log.prototype.log = function(level, items) {
|
Log.prototype.log = function(level, items) {
|
||||||
var i, item, len, logString;
|
var item, j, len, logString;
|
||||||
items.unshift('||');
|
items.unshift('||');
|
||||||
items.unshift(level);
|
items.unshift(level);
|
||||||
items.unshift(this.options.logPrefix);
|
items.unshift(this.options.logPrefix);
|
||||||
|
@ -69,8 +130,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logString = '';
|
logString = '';
|
||||||
for (i = 0, len = items.length; i < len; i++) {
|
for (j = 0, len = items.length; j < len; j++) {
|
||||||
item = items[i];
|
item = items[j];
|
||||||
logString += ' ';
|
logString += ' ';
|
||||||
if (typeof item === 'object') {
|
if (typeof item === 'object') {
|
||||||
logString += JSON.stringify(item);
|
logString += JSON.stringify(item);
|
||||||
|
@ -173,11 +234,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
})(this);
|
})(this);
|
||||||
this.ws.onmessage = (function(_this) {
|
this.ws.onmessage = (function(_this) {
|
||||||
return function(e) {
|
return function(e) {
|
||||||
var i, len, pipe, pipes;
|
var j, len, pipe, pipes;
|
||||||
pipes = JSON.parse(e.data);
|
pipes = JSON.parse(e.data);
|
||||||
_this.log.debug('onMessage', e.data);
|
_this.log.debug('onMessage', e.data);
|
||||||
for (i = 0, len = pipes.length; i < len; i++) {
|
for (j = 0, len = pipes.length; j < len; j++) {
|
||||||
pipe = pipes[i];
|
pipe = pipes[j];
|
||||||
if (pipe.event === 'pong') {
|
if (pipe.event === 'pong') {
|
||||||
_this.ping();
|
_this.ping();
|
||||||
}
|
}
|
||||||
|
@ -386,8 +447,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
|
|
||||||
ZammadChat.prototype.scrollSnapTolerance = 10;
|
ZammadChat.prototype.scrollSnapTolerance = 10;
|
||||||
|
|
||||||
|
ZammadChat.prototype.richTextFormatKey = {
|
||||||
|
66: true,
|
||||||
|
73: true,
|
||||||
|
85: true,
|
||||||
|
83: true
|
||||||
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.T = function() {
|
ZammadChat.prototype.T = function() {
|
||||||
var i, item, items, len, string, translations;
|
var item, items, j, len, string, translations;
|
||||||
string = arguments[0], items = 2 <= arguments.length ? slice.call(arguments, 1) : [];
|
string = arguments[0], items = 2 <= arguments.length ? slice.call(arguments, 1) : [];
|
||||||
if (this.options.lang && this.options.lang !== 'en') {
|
if (this.options.lang && this.options.lang !== 'en') {
|
||||||
if (!this.translations[this.options.lang]) {
|
if (!this.translations[this.options.lang]) {
|
||||||
|
@ -401,8 +469,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (items) {
|
if (items) {
|
||||||
for (i = 0, len = items.length; i < len; i++) {
|
for (j = 0, len = items.length; j < len; j++) {
|
||||||
item = items[i];
|
item = items[j];
|
||||||
string = string.replace(/%s/, item);
|
string = string.replace(/%s/, item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -425,6 +493,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
function ZammadChat(options) {
|
function ZammadChat(options) {
|
||||||
|
this.removeAttributes = bind(this.removeAttributes, this);
|
||||||
this.startTimeoutObservers = bind(this.startTimeoutObservers, this);
|
this.startTimeoutObservers = bind(this.startTimeoutObservers, this);
|
||||||
this.onCssLoaded = bind(this.onCssLoaded, this);
|
this.onCssLoaded = bind(this.onCssLoaded, this);
|
||||||
this.setAgentOnlineState = bind(this.setAgentOnlineState, this);
|
this.setAgentOnlineState = bind(this.setAgentOnlineState, this);
|
||||||
|
@ -552,6 +621,203 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
keydown: this.checkForEnter,
|
keydown: this.checkForEnter,
|
||||||
input: this.onInput
|
input: this.onInput
|
||||||
});
|
});
|
||||||
|
this.input.on('keydown', (function(_this) {
|
||||||
|
return function(e) {
|
||||||
|
var richtTextControl;
|
||||||
|
richtTextControl = false;
|
||||||
|
if (!e.altKey && !e.ctrlKey && e.metaKey) {
|
||||||
|
richtTextControl = true;
|
||||||
|
} else if (!e.altKey && e.ctrlKey && !e.metaKey) {
|
||||||
|
richtTextControl = true;
|
||||||
|
}
|
||||||
|
if (richtTextControl && _this.richTextFormatKey[e.keyCode]) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.keyCode === 66) {
|
||||||
|
document.execCommand('bold');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 73) {
|
||||||
|
document.execCommand('italic');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 85) {
|
||||||
|
document.execCommand('underline');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 83) {
|
||||||
|
document.execCommand('strikeThrough');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(this));
|
||||||
|
this.input.on('paste', (function(_this) {
|
||||||
|
return function(e) {
|
||||||
|
var clipboardData, docType, error, html, htmlTmp, imageFile, imageInserted, item, match, reader, regex, replacementTag, text;
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
clipboardData;
|
||||||
|
if (e.clipboardData) {
|
||||||
|
clipboardData = e.clipboardData;
|
||||||
|
} else if (window.clipboardData) {
|
||||||
|
clipboardData = window.clipboardData;
|
||||||
|
} else if (e.originalEvent.clipboardData) {
|
||||||
|
clipboardData = e.originalEvent.clipboardData;
|
||||||
|
} else {
|
||||||
|
throw 'No clipboardData support';
|
||||||
|
}
|
||||||
|
imageInserted = false;
|
||||||
|
if (clipboardData && clipboardData.items && clipboardData.items[0]) {
|
||||||
|
item = clipboardData.items[0];
|
||||||
|
if (item.kind === 'file' && (item.type === 'image/png' || item.type === 'image/jpeg')) {
|
||||||
|
imageFile = item.getAsFile();
|
||||||
|
reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
var img, insert, result;
|
||||||
|
result = e.target.result;
|
||||||
|
img = document.createElement('img');
|
||||||
|
img.src = result;
|
||||||
|
insert = function(dataUrl, width, height, isRetina) {
|
||||||
|
if (_this.isRetina()) {
|
||||||
|
width = width / 2;
|
||||||
|
height = height / 2;
|
||||||
|
}
|
||||||
|
result = dataUrl;
|
||||||
|
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">";
|
||||||
|
return document.execCommand('insertHTML', false, img);
|
||||||
|
};
|
||||||
|
return _this.resizeImage(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(imageFile);
|
||||||
|
imageInserted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (imageInserted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
text = void 0;
|
||||||
|
docType = void 0;
|
||||||
|
try {
|
||||||
|
text = clipboardData.getData('text/html');
|
||||||
|
docType = 'html';
|
||||||
|
if (!text || text.length === 0) {
|
||||||
|
docType = 'text';
|
||||||
|
text = clipboardData.getData('text/plain');
|
||||||
|
}
|
||||||
|
if (!text || text.length === 0) {
|
||||||
|
docType = 'text2';
|
||||||
|
text = clipboardData.getData('text');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
e = error;
|
||||||
|
console.log('Sorry, can\'t insert markup because browser is not supporting it.');
|
||||||
|
docType = 'text3';
|
||||||
|
text = clipboardData.getData('text');
|
||||||
|
}
|
||||||
|
if (docType === 'text' || docType === 'text2' || docType === 'text3') {
|
||||||
|
text = '<div>' + text.replace(/\n/g, '</div><div>') + '</div>';
|
||||||
|
text = text.replace(/<div><\/div>/g, '<div><br></div>');
|
||||||
|
}
|
||||||
|
console.log('p', docType, text);
|
||||||
|
if (docType === 'html') {
|
||||||
|
html = $("<div>" + text + "</div>");
|
||||||
|
match = false;
|
||||||
|
htmlTmp = text;
|
||||||
|
regex = new RegExp('<(/w|w)\:[A-Za-z]');
|
||||||
|
if (htmlTmp.match(regex)) {
|
||||||
|
match = true;
|
||||||
|
htmlTmp = htmlTmp.replace(regex, '');
|
||||||
|
}
|
||||||
|
regex = new RegExp('<(/o|o)\:[A-Za-z]');
|
||||||
|
if (htmlTmp.match(regex)) {
|
||||||
|
match = true;
|
||||||
|
htmlTmp = htmlTmp.replace(regex, '');
|
||||||
|
}
|
||||||
|
if (match) {
|
||||||
|
html = _this.wordFilter(html);
|
||||||
|
}
|
||||||
|
html = $(html);
|
||||||
|
html.contents().each(function() {
|
||||||
|
if (this.nodeType === 8) {
|
||||||
|
return $(this).remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html.find('a, font, small, time, form, label').replaceWith(function() {
|
||||||
|
return $(this).contents();
|
||||||
|
});
|
||||||
|
replacementTag = 'div';
|
||||||
|
html.find('textarea').each(function() {
|
||||||
|
var newTag, outer;
|
||||||
|
outer = this.outerHTML;
|
||||||
|
regex = new RegExp('<' + this.tagName, 'i');
|
||||||
|
newTag = outer.replace(regex, '<' + replacementTag);
|
||||||
|
regex = new RegExp('</' + this.tagName, 'i');
|
||||||
|
newTag = newTag.replace(regex, '</' + replacementTag);
|
||||||
|
return $(this).replaceWith(newTag);
|
||||||
|
});
|
||||||
|
html.find('font, img, svg, input, select, button, style, applet, embed, noframes, canvas, script, frame, iframe, meta, link, title, head, fieldset').remove();
|
||||||
|
_this.removeAttributes(html);
|
||||||
|
text = html.html();
|
||||||
|
}
|
||||||
|
if (docType === 'text3') {
|
||||||
|
_this.pasteHtmlAtCaret(text);
|
||||||
|
} else {
|
||||||
|
document.execCommand('insertHTML', false, text);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
})(this));
|
||||||
|
this.input.on('drop', (function(_this) {
|
||||||
|
return function(e) {
|
||||||
|
var dataTransfer, file, reader, x, y;
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
dataTransfer;
|
||||||
|
if (window.dataTransfer) {
|
||||||
|
dataTransfer = window.dataTransfer;
|
||||||
|
} else if (e.originalEvent.dataTransfer) {
|
||||||
|
dataTransfer = e.originalEvent.dataTransfer;
|
||||||
|
} else {
|
||||||
|
throw 'No clipboardData support';
|
||||||
|
}
|
||||||
|
x = e.clientX;
|
||||||
|
y = e.clientY;
|
||||||
|
file = dataTransfer.files[0];
|
||||||
|
if (file.type.match('image.*')) {
|
||||||
|
reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
var img, insert, result;
|
||||||
|
result = e.target.result;
|
||||||
|
img = document.createElement('img');
|
||||||
|
img.src = result;
|
||||||
|
insert = function(dataUrl, width, height, isRetina) {
|
||||||
|
var pos, range;
|
||||||
|
if (_this.isRetina()) {
|
||||||
|
width = width / 2;
|
||||||
|
height = height / 2;
|
||||||
|
}
|
||||||
|
result = dataUrl;
|
||||||
|
img = $("<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">");
|
||||||
|
img = img.get(0);
|
||||||
|
if (document.caretPositionFromPoint) {
|
||||||
|
pos = document.caretPositionFromPoint(x, y);
|
||||||
|
range = document.createRange();
|
||||||
|
range.setStart(pos.offsetNode, pos.offset);
|
||||||
|
range.collapse();
|
||||||
|
return range.insertNode(img);
|
||||||
|
} else if (document.caretRangeFromPoint) {
|
||||||
|
range = document.caretRangeFromPoint(x, y);
|
||||||
|
return range.insertNode(img);
|
||||||
|
} else {
|
||||||
|
return console.log('could not find carat');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return _this.resizeImage(img.src, 460, 'auto', 2, 'image/jpeg', 'auto', insert);
|
||||||
|
};
|
||||||
|
return reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})(this));
|
||||||
$(window).on('beforeunload', (function(_this) {
|
$(window).on('beforeunload', (function(_this) {
|
||||||
return function() {
|
return function() {
|
||||||
return _this.onLeaveTemporary();
|
return _this.onLeaveTemporary();
|
||||||
|
@ -595,9 +861,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.onWebSocketMessage = function(pipes) {
|
ZammadChat.prototype.onWebSocketMessage = function(pipes) {
|
||||||
var i, len, pipe;
|
var j, len, pipe;
|
||||||
for (i = 0, len = pipes.length; i < len; i++) {
|
for (j = 0, len = pipes.length; j < len; j++) {
|
||||||
pipe = pipes[i];
|
pipe = pipes[j];
|
||||||
this.log.debug('ws:onmessage', pipe);
|
this.log.debug('ws:onmessage', pipe);
|
||||||
switch (pipe.event) {
|
switch (pipe.event) {
|
||||||
case 'chat_error':
|
case 'chat_error':
|
||||||
|
@ -683,15 +949,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
};
|
};
|
||||||
|
|
||||||
ZammadChat.prototype.onReopenSession = function(data) {
|
ZammadChat.prototype.onReopenSession = function(data) {
|
||||||
var i, len, message, ref, unfinishedMessage;
|
var j, len, message, ref, unfinishedMessage;
|
||||||
this.log.debug('old messages', data.session);
|
this.log.debug('old messages', data.session);
|
||||||
this.inactiveTimeout.start();
|
this.inactiveTimeout.start();
|
||||||
unfinishedMessage = sessionStorage.getItem('unfinished_message');
|
unfinishedMessage = sessionStorage.getItem('unfinished_message');
|
||||||
if (data.agent) {
|
if (data.agent) {
|
||||||
this.onConnectionEstablished(data);
|
this.onConnectionEstablished(data);
|
||||||
ref = data.session;
|
ref = data.session;
|
||||||
for (i = 0, len = ref.length; i < len; i++) {
|
for (j = 0, len = ref.length; j < len; j++) {
|
||||||
message = ref[i];
|
message = ref[j];
|
||||||
this.renderMessage({
|
this.renderMessage({
|
||||||
message: message.content,
|
message: message.content,
|
||||||
id: message.id,
|
id: message.id,
|
||||||
|
@ -699,7 +965,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (unfinishedMessage) {
|
if (unfinishedMessage) {
|
||||||
this.input.val(unfinishedMessage);
|
this.input.html(unfinishedMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.position) {
|
if (data.position) {
|
||||||
|
@ -715,7 +981,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
|
|
||||||
ZammadChat.prototype.onInput = function() {
|
ZammadChat.prototype.onInput = function() {
|
||||||
this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread');
|
this.el.find('.zammad-chat-message--unread').removeClass('zammad-chat-message--unread');
|
||||||
sessionStorage.setItem('unfinished_message', this.input.val());
|
sessionStorage.setItem('unfinished_message', this.input.html());
|
||||||
return this.onTyping();
|
return this.onTyping();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -749,7 +1015,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
|
|
||||||
ZammadChat.prototype.sendMessage = function() {
|
ZammadChat.prototype.sendMessage = function() {
|
||||||
var message, messageElement;
|
var message, messageElement;
|
||||||
message = this.input.val();
|
message = this.input.html();
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -769,7 +1035,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
this.lastAddedType = 'message--customer';
|
this.lastAddedType = 'message--customer';
|
||||||
this.el.find('.zammad-chat-body').append(messageElement);
|
this.el.find('.zammad-chat-body').append(messageElement);
|
||||||
}
|
}
|
||||||
this.input.val('');
|
this.input.html('');
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
return this.send('chat_session_message', {
|
return this.send('chat_session_message', {
|
||||||
content: message,
|
content: message,
|
||||||
|
@ -810,12 +1076,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
this.showLoader();
|
this.showLoader();
|
||||||
}
|
}
|
||||||
this.el.addClass('zammad-chat-is-open');
|
this.el.addClass('zammad-chat-is-open');
|
||||||
if (!this.inputInitialized) {
|
|
||||||
this.inputInitialized = true;
|
|
||||||
this.input.autoGrow({
|
|
||||||
extraLine: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
remainerHeight = this.el.height() - this.el.find('.zammad-chat-header').outerHeight();
|
remainerHeight = this.el.height() - this.el.find('.zammad-chat-header').outerHeight();
|
||||||
this.el.css('bottom', -remainerHeight);
|
this.el.css('bottom', -remainerHeight);
|
||||||
if (!this.sessionId) {
|
if (!this.sessionId) {
|
||||||
|
@ -1328,165 +1588,223 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ZammadChat.prototype.isRetina = function() {
|
||||||
|
var mq;
|
||||||
|
if (window.matchMedia) {
|
||||||
|
mq = window.matchMedia('only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 1.3dppx)');
|
||||||
|
return mq && mq.matches || (window.devicePixelRatio > 1);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ZammadChat.prototype.resizeImage = function(dataURL, x, y, sizeFactor, type, quallity, callback, force) {
|
||||||
|
var imageObject;
|
||||||
|
if (x == null) {
|
||||||
|
x = 'auto';
|
||||||
|
}
|
||||||
|
if (y == null) {
|
||||||
|
y = 'auto';
|
||||||
|
}
|
||||||
|
if (sizeFactor == null) {
|
||||||
|
sizeFactor = 1;
|
||||||
|
}
|
||||||
|
if (force == null) {
|
||||||
|
force = true;
|
||||||
|
}
|
||||||
|
imageObject = new Image();
|
||||||
|
imageObject.onload = function() {
|
||||||
|
var canvas, context, factor, imageHeight, imageWidth, newDataUrl, resize;
|
||||||
|
imageWidth = imageObject.width;
|
||||||
|
imageHeight = imageObject.height;
|
||||||
|
console.log('ImageService', 'current size', imageWidth, imageHeight);
|
||||||
|
if (y === 'auto' && x === 'auto') {
|
||||||
|
x = imageWidth;
|
||||||
|
y = imageHeight;
|
||||||
|
}
|
||||||
|
if (y === 'auto') {
|
||||||
|
factor = imageWidth / x;
|
||||||
|
y = imageHeight / factor;
|
||||||
|
}
|
||||||
|
if (x === 'auto') {
|
||||||
|
factor = imageWidth / y;
|
||||||
|
x = imageHeight / factor;
|
||||||
|
}
|
||||||
|
resize = false;
|
||||||
|
if (x < imageWidth || y < imageHeight) {
|
||||||
|
resize = true;
|
||||||
|
x = x * sizeFactor;
|
||||||
|
y = y * sizeFactor;
|
||||||
|
} else {
|
||||||
|
x = imageWidth;
|
||||||
|
y = imageHeight;
|
||||||
|
}
|
||||||
|
canvas = document.createElement('canvas');
|
||||||
|
canvas.width = x;
|
||||||
|
canvas.height = y;
|
||||||
|
context = canvas.getContext('2d');
|
||||||
|
context.drawImage(imageObject, 0, 0, x, y);
|
||||||
|
if (quallity === 'auto') {
|
||||||
|
if (x < 200 && y < 200) {
|
||||||
|
quallity = 1;
|
||||||
|
} else if (x < 400 && y < 400) {
|
||||||
|
quallity = 0.9;
|
||||||
|
} else if (x < 600 && y < 600) {
|
||||||
|
quallity = 0.8;
|
||||||
|
} else if (x < 900 && y < 900) {
|
||||||
|
quallity = 0.7;
|
||||||
|
} else {
|
||||||
|
quallity = 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newDataUrl = canvas.toDataURL(type, quallity);
|
||||||
|
if (resize) {
|
||||||
|
console.log('ImageService', 'resize', x / sizeFactor, y / sizeFactor, quallity, (newDataUrl.length * 0.75) / 1024 / 1024, 'in mb');
|
||||||
|
callback(newDataUrl, x / sizeFactor, y / sizeFactor, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('ImageService', 'no resize', x, y, quallity, (newDataUrl.length * 0.75) / 1024 / 1024, 'in mb');
|
||||||
|
return callback(newDataUrl, x, y, false);
|
||||||
|
};
|
||||||
|
return imageObject.src = dataURL;
|
||||||
|
};
|
||||||
|
|
||||||
|
ZammadChat.prototype.pasteHtmlAtCaret = function(html) {
|
||||||
|
var el, frag, lastNode, node, range, sel;
|
||||||
|
sel = void 0;
|
||||||
|
range = void 0;
|
||||||
|
if (window.getSelection) {
|
||||||
|
sel = window.getSelection();
|
||||||
|
if (sel.getRangeAt && sel.rangeCount) {
|
||||||
|
range = sel.getRangeAt(0);
|
||||||
|
range.deleteContents();
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.innerHTML = html;
|
||||||
|
frag = document.createDocumentFragment(node, lastNode);
|
||||||
|
while (node = el.firstChild) {
|
||||||
|
lastNode = frag.appendChild(node);
|
||||||
|
}
|
||||||
|
range.insertNode(frag);
|
||||||
|
if (lastNode) {
|
||||||
|
range = range.cloneRange();
|
||||||
|
range.setStartAfter(lastNode);
|
||||||
|
range.collapse(true);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
return sel.addRange(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (document.selection && document.selection.type !== 'Control') {
|
||||||
|
return document.selection.createRange().pasteHTML(html);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ZammadChat.prototype.wordFilter = function(editor) {
|
||||||
|
var content, last_level, pnt;
|
||||||
|
content = editor.html();
|
||||||
|
content = content.replace(/<!--[\s\S]+?-->/gi, '');
|
||||||
|
content = content.replace(/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi, '');
|
||||||
|
content = content.replace(/<(\/?)s>/gi, '<$1strike>');
|
||||||
|
content = content.replace(/ /gi, ' ');
|
||||||
|
editor.html(content);
|
||||||
|
$('p', editor).each(function() {
|
||||||
|
var matches, str;
|
||||||
|
str = $(this).attr('style');
|
||||||
|
matches = /mso-list:\w+ \w+([0-9]+)/.exec(str);
|
||||||
|
if (matches) {
|
||||||
|
return $(this).data('_listLevel', parseInt(matches[1], 10));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
last_level = 0;
|
||||||
|
pnt = null;
|
||||||
|
$('p', editor).each(function() {
|
||||||
|
var cur_level, i, j, list_tag, matches, ref, ref1, ref2, start, txt;
|
||||||
|
cur_level = $(this).data('_listLevel');
|
||||||
|
if (cur_level !== void 0) {
|
||||||
|
txt = $(this).text();
|
||||||
|
list_tag = '<ul></ul>';
|
||||||
|
if (/^\s*\w+\./.test(txt)) {
|
||||||
|
matches = /([0-9])\./.exec(txt);
|
||||||
|
if (matches) {
|
||||||
|
start = parseInt(matches[1], 10);
|
||||||
|
list_tag = (ref = start > 1) != null ? ref : '<ol start="' + start + {
|
||||||
|
'"></ol>': '<ol></ol>'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
list_tag = '<ol></ol>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cur_level > last_level) {
|
||||||
|
if (last_level === 0) {
|
||||||
|
$(this).before(list_tag);
|
||||||
|
pnt = $(this).prev();
|
||||||
|
} else {
|
||||||
|
pnt = $(list_tag).appendTo(pnt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cur_level < last_level) {
|
||||||
|
for (i = j = ref1 = i, ref2 = last_level - cur_level; ref1 <= ref2 ? j <= ref2 : j >= ref2; i = ref1 <= ref2 ? ++j : --j) {
|
||||||
|
pnt = pnt.parent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$('span:first', this).remove();
|
||||||
|
pnt.append('<li>' + $(this).html() + '</li>');
|
||||||
|
$(this).remove();
|
||||||
|
return last_level = cur_level;
|
||||||
|
} else {
|
||||||
|
return last_level = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('[style]', editor).removeAttr('style');
|
||||||
|
$('[align]', editor).removeAttr('align');
|
||||||
|
$('span', editor).replaceWith(function() {
|
||||||
|
return $(this).contents();
|
||||||
|
});
|
||||||
|
$('span:empty', editor).remove();
|
||||||
|
$("[class^='Mso']", editor).removeAttr('class');
|
||||||
|
$('p:empty', editor).remove();
|
||||||
|
return editor;
|
||||||
|
};
|
||||||
|
|
||||||
|
ZammadChat.prototype.removeAttribute = function(element) {
|
||||||
|
var $element, att, j, len, ref;
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$element = $(element);
|
||||||
|
ref = element.attributes;
|
||||||
|
for (j = 0, len = ref.length; j < len; j++) {
|
||||||
|
att = ref[j];
|
||||||
|
if (att && att.name) {
|
||||||
|
element.removeAttribute(att.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $element.removeAttr('style').removeAttr('class').removeAttr('lang').removeAttr('type').removeAttr('align').removeAttr('id').removeAttr('wrap').removeAttr('title');
|
||||||
|
};
|
||||||
|
|
||||||
|
ZammadChat.prototype.removeAttributes = function(html, parent) {
|
||||||
|
if (parent == null) {
|
||||||
|
parent = true;
|
||||||
|
}
|
||||||
|
if (parent) {
|
||||||
|
html.each((function(_this) {
|
||||||
|
return function(index, element) {
|
||||||
|
return _this.removeAttribute(element);
|
||||||
|
};
|
||||||
|
})(this));
|
||||||
|
}
|
||||||
|
html.find('*').each((function(_this) {
|
||||||
|
return function(index, element) {
|
||||||
|
return _this.removeAttribute(element);
|
||||||
|
};
|
||||||
|
})(this));
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
return ZammadChat;
|
return ZammadChat;
|
||||||
|
|
||||||
})(Base);
|
})(Base);
|
||||||
return window.ZammadChat = ZammadChat;
|
return window.ZammadChat = ZammadChat;
|
||||||
})(window.jQuery, window);
|
})(window.jQuery, window);
|
||||||
|
|
||||||
if (!window.zammadChatTemplates) {
|
|
||||||
window.zammadChatTemplates = {};
|
|
||||||
}
|
|
||||||
window.zammadChatTemplates["agent"] = function (__obj) {
|
|
||||||
if (!__obj) __obj = {};
|
|
||||||
var __out = [], __capture = function(callback) {
|
|
||||||
var out = __out, result;
|
|
||||||
__out = [];
|
|
||||||
callback.call(this);
|
|
||||||
result = __out.join('');
|
|
||||||
__out = out;
|
|
||||||
return __safe(result);
|
|
||||||
}, __sanitize = function(value) {
|
|
||||||
if (value && value.ecoSafe) {
|
|
||||||
return value;
|
|
||||||
} else if (typeof value !== 'undefined' && value != null) {
|
|
||||||
return __escape(value);
|
|
||||||
} else {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}, __safe, __objSafe = __obj.safe, __escape = __obj.escape;
|
|
||||||
__safe = __obj.safe = function(value) {
|
|
||||||
if (value && value.ecoSafe) {
|
|
||||||
return value;
|
|
||||||
} else {
|
|
||||||
if (!(typeof value !== 'undefined' && value != null)) value = '';
|
|
||||||
var result = new String(value);
|
|
||||||
result.ecoSafe = true;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (!__escape) {
|
|
||||||
__escape = __obj.escape = function(value) {
|
|
||||||
return ('' + value)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
};
|
|
||||||
}
|
|
||||||
(function() {
|
|
||||||
(function() {
|
|
||||||
if (this.agent.avatar) {
|
|
||||||
__out.push('\n<img class="zammad-chat-agent-avatar" src="');
|
|
||||||
__out.push(__sanitize(this.agent.avatar));
|
|
||||||
__out.push('">\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
__out.push('\n<span class="zammad-chat-agent-sentence">\n <span class="zammad-chat-agent-name">');
|
|
||||||
|
|
||||||
__out.push(__sanitize(this.agent.name));
|
|
||||||
|
|
||||||
__out.push('</span>\n</span>');
|
|
||||||
|
|
||||||
}).call(this);
|
|
||||||
|
|
||||||
}).call(__obj);
|
|
||||||
__obj.safe = __objSafe, __obj.escape = __escape;
|
|
||||||
return __out.join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
|
||||||
* <jevin9@gmail.com> wrote this file. As long as you retain this notice you
|
|
||||||
* can do whatever you want with this stuff. If we meet some day, and you think
|
|
||||||
* this stuff is worth it, you can buy me a beer in return. Jevin O. Sewaruth
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
*
|
|
||||||
* Autogrow Textarea Plugin Version v3.0
|
|
||||||
* http://www.technoreply.com/autogrow-textarea-plugin-3-0
|
|
||||||
*
|
|
||||||
* THIS PLUGIN IS DELIVERD ON A PAY WHAT YOU WHANT BASIS. IF THE PLUGIN WAS USEFUL TO YOU, PLEASE CONSIDER BUYING THE PLUGIN HERE :
|
|
||||||
* https://sites.fastspring.com/technoreply/instant/autogrowtextareaplugin
|
|
||||||
*
|
|
||||||
* Date: October 15, 2012
|
|
||||||
*
|
|
||||||
* Zammad modification:
|
|
||||||
* - remove overflow:hidden when maximum height is reached
|
|
||||||
* - mirror box-sizing
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
jQuery.fn.autoGrow = function(options) {
|
|
||||||
return this.each(function() {
|
|
||||||
var settings = jQuery.extend({
|
|
||||||
extraLine: true,
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
var createMirror = function(textarea) {
|
|
||||||
jQuery(textarea).after('<div class="autogrow-textarea-mirror"></div>');
|
|
||||||
return jQuery(textarea).next('.autogrow-textarea-mirror')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendContentToMirror = function (textarea) {
|
|
||||||
mirror.innerHTML = String(textarea.value)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/\n/g, '<br />') +
|
|
||||||
(settings.extraLine? '.<br/>.' : '')
|
|
||||||
;
|
|
||||||
|
|
||||||
if (jQuery(textarea).height() != jQuery(mirror).height()) {
|
|
||||||
jQuery(textarea).height(jQuery(mirror).height());
|
|
||||||
|
|
||||||
var overflow = jQuery(mirror).height() > maxHeight ? '' : 'hidden';
|
|
||||||
jQuery(textarea).css('overflow', overflow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var growTextarea = function () {
|
|
||||||
sendContentToMirror(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a mirror
|
|
||||||
var mirror = createMirror(this);
|
|
||||||
|
|
||||||
// Store max-height
|
|
||||||
var maxHeight = parseInt(jQuery(this).css('max-height'), 10);
|
|
||||||
|
|
||||||
// Style the mirror
|
|
||||||
mirror.style.display = 'none';
|
|
||||||
mirror.style.wordWrap = 'break-word';
|
|
||||||
mirror.style.whiteSpace = 'normal';
|
|
||||||
mirror.style.padding = jQuery(this).css('paddingTop') + ' ' +
|
|
||||||
jQuery(this).css('paddingRight') + ' ' +
|
|
||||||
jQuery(this).css('paddingBottom') + ' ' +
|
|
||||||
jQuery(this).css('paddingLeft');
|
|
||||||
|
|
||||||
mirror.style.width = jQuery(this).css('width');
|
|
||||||
mirror.style.fontFamily = jQuery(this).css('font-family');
|
|
||||||
mirror.style.fontSize = jQuery(this).css('font-size');
|
|
||||||
mirror.style.lineHeight = jQuery(this).css('line-height');
|
|
||||||
mirror.style.letterSpacing = jQuery(this).css('letter-spacing');
|
|
||||||
mirror.style.boxSizing = jQuery(this).css('boxSizing');
|
|
||||||
|
|
||||||
// Style the textarea
|
|
||||||
this.style.overflow = "hidden";
|
|
||||||
this.style.minHeight = this.rows+"em";
|
|
||||||
|
|
||||||
// Bind the textarea's event
|
|
||||||
this.onkeyup = growTextarea;
|
|
||||||
this.onfocus = growTextarea;
|
|
||||||
|
|
||||||
// Fire the event for text already present
|
|
||||||
sendContentToMirror(this);
|
|
||||||
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if (!window.zammadChatTemplates) {
|
if (!window.zammadChatTemplates) {
|
||||||
window.zammadChatTemplates = {};
|
window.zammadChatTemplates = {};
|
||||||
}
|
}
|
||||||
|
@ -1555,11 +1873,11 @@ window.zammadChatTemplates["chat"] = function (__obj) {
|
||||||
|
|
||||||
__out.push(this.T(this.scrollHint));
|
__out.push(this.T(this.scrollHint));
|
||||||
|
|
||||||
__out.push('\n </div>\n <div class="zammad-chat-body"></div>\n <form class="zammad-chat-controls">\n <textarea class="zammad-chat-input" rows="1" placeholder="');
|
__out.push('\n </div>\n <div class="zammad-chat-body"></div>\n <form class="zammad-chat-controls">\n <div class="zammad-chat-input" rows="1" placeholder="');
|
||||||
|
|
||||||
__out.push(this.T('Compose your message...'));
|
__out.push(this.T('Compose your message...'));
|
||||||
|
|
||||||
__out.push('"></textarea>\n <button type="submit" class="zammad-chat-button zammad-chat-send"');
|
__out.push('" contenteditable="true"></div>\n <button type="submit" class="zammad-chat-button zammad-chat-send"');
|
||||||
|
|
||||||
if (this.background) {
|
if (this.background) {
|
||||||
__out.push(__sanitize(" style='background: " + this.background + "'"));
|
__out.push(__sanitize(" style='background: " + this.background + "'"));
|
||||||
|
|
4
public/assets/chat/chat.min.js
vendored
4
public/assets/chat/chat.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -329,7 +329,7 @@
|
||||||
.zammad-chat-controls {
|
.zammad-chat-controls {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: none;
|
display: none;
|
||||||
align-items: flex-start;
|
align-items: flex-end;
|
||||||
border-top: 1px solid hsl(0,0%,93%);
|
border-top: 1px solid hsl(0,0%,93%);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -349,21 +349,19 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 1em 2em;
|
padding: 1em 2em;
|
||||||
float: left;
|
float: left;
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
max-height: 6em;
|
max-height: 6em;
|
||||||
min-height: 1.4em !important;
|
min-height: 1.4em;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
line-height: 1.4em;
|
line-height: 1.4em;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: none !important;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
box-shadow: none !important;
|
box-shadow: none ;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: none;
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zammad-chat-input::-webkit-input-placeholder {
|
.zammad-chat-input::-webkit-input-placeholder {
|
||||||
|
@ -378,7 +376,7 @@
|
||||||
background: hsl(203,67%,53%);
|
background: hsl(203,67%,53%);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.5em 1.2em;
|
padding: 0.5em 1.2em;
|
||||||
margin: 0.5em 1em 0.5em;
|
margin: 0.63em 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 1.5em;
|
border-radius: 1.5em;
|
||||||
|
|
|
@ -29,9 +29,7 @@ gulp.task('js', function(){
|
||||||
.pipe(plumber())
|
.pipe(plumber())
|
||||||
.pipe(coffee({bare: true}).on('error', gutil.log));
|
.pipe(coffee({bare: true}).on('error', gutil.log));
|
||||||
|
|
||||||
var autoGrow = gulp.src('jquery.autoGrow.js');
|
return merge(templates, js)
|
||||||
|
|
||||||
return merge(templates, js, autoGrow)
|
|
||||||
.pipe(concat('chat.js'))
|
.pipe(concat('chat.js'))
|
||||||
.pipe(gulp.dest('./'))
|
.pipe(gulp.dest('./'))
|
||||||
.pipe(uglify())
|
.pipe(uglify())
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
/*!
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
* "THE BEER-WARE LICENSE" (Revision 42):
|
|
||||||
* <jevin9@gmail.com> wrote this file. As long as you retain this notice you
|
|
||||||
* can do whatever you want with this stuff. If we meet some day, and you think
|
|
||||||
* this stuff is worth it, you can buy me a beer in return. Jevin O. Sewaruth
|
|
||||||
* ----------------------------------------------------------------------------
|
|
||||||
*
|
|
||||||
* Autogrow Textarea Plugin Version v3.0
|
|
||||||
* http://www.technoreply.com/autogrow-textarea-plugin-3-0
|
|
||||||
*
|
|
||||||
* THIS PLUGIN IS DELIVERD ON A PAY WHAT YOU WHANT BASIS. IF THE PLUGIN WAS USEFUL TO YOU, PLEASE CONSIDER BUYING THE PLUGIN HERE :
|
|
||||||
* https://sites.fastspring.com/technoreply/instant/autogrowtextareaplugin
|
|
||||||
*
|
|
||||||
* Date: October 15, 2012
|
|
||||||
*
|
|
||||||
* Zammad modification:
|
|
||||||
* - remove overflow:hidden when maximum height is reached
|
|
||||||
* - mirror box-sizing
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
jQuery.fn.autoGrow = function(options) {
|
|
||||||
return this.each(function() {
|
|
||||||
var settings = jQuery.extend({
|
|
||||||
extraLine: true,
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
var createMirror = function(textarea) {
|
|
||||||
jQuery(textarea).after('<div class="autogrow-textarea-mirror"></div>');
|
|
||||||
return jQuery(textarea).next('.autogrow-textarea-mirror')[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendContentToMirror = function (textarea) {
|
|
||||||
mirror.innerHTML = String(textarea.value)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/\n/g, '<br />') +
|
|
||||||
(settings.extraLine? '.<br/>.' : '')
|
|
||||||
;
|
|
||||||
|
|
||||||
if (jQuery(textarea).height() != jQuery(mirror).height()) {
|
|
||||||
jQuery(textarea).height(jQuery(mirror).height());
|
|
||||||
|
|
||||||
var overflow = jQuery(mirror).height() > maxHeight ? '' : 'hidden';
|
|
||||||
jQuery(textarea).css('overflow', overflow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var growTextarea = function () {
|
|
||||||
sendContentToMirror(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a mirror
|
|
||||||
var mirror = createMirror(this);
|
|
||||||
|
|
||||||
// Store max-height
|
|
||||||
var maxHeight = parseInt(jQuery(this).css('max-height'), 10);
|
|
||||||
|
|
||||||
// Style the mirror
|
|
||||||
mirror.style.display = 'none';
|
|
||||||
mirror.style.wordWrap = 'break-word';
|
|
||||||
mirror.style.whiteSpace = 'normal';
|
|
||||||
mirror.style.padding = jQuery(this).css('paddingTop') + ' ' +
|
|
||||||
jQuery(this).css('paddingRight') + ' ' +
|
|
||||||
jQuery(this).css('paddingBottom') + ' ' +
|
|
||||||
jQuery(this).css('paddingLeft');
|
|
||||||
|
|
||||||
mirror.style.width = jQuery(this).css('width');
|
|
||||||
mirror.style.fontFamily = jQuery(this).css('font-family');
|
|
||||||
mirror.style.fontSize = jQuery(this).css('font-size');
|
|
||||||
mirror.style.lineHeight = jQuery(this).css('line-height');
|
|
||||||
mirror.style.letterSpacing = jQuery(this).css('letter-spacing');
|
|
||||||
mirror.style.boxSizing = jQuery(this).css('boxSizing');
|
|
||||||
|
|
||||||
// Style the textarea
|
|
||||||
this.style.overflow = "hidden";
|
|
||||||
this.style.minHeight = this.rows+"em";
|
|
||||||
|
|
||||||
// Bind the textarea's event
|
|
||||||
this.onkeyup = growTextarea;
|
|
||||||
this.onfocus = growTextarea;
|
|
||||||
|
|
||||||
// Fire the event for text already present
|
|
||||||
sendContentToMirror(this);
|
|
||||||
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -21,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="zammad-chat-body"></div>
|
<div class="zammad-chat-body"></div>
|
||||||
<form class="zammad-chat-controls">
|
<form class="zammad-chat-controls">
|
||||||
<textarea class="zammad-chat-input" rows="1" placeholder="<%- @T('Compose your message...') %>"></textarea>
|
<div class="zammad-chat-input" rows="1" placeholder="<%- @T('Compose your message...') %>" contenteditable="true"></div>
|
||||||
<button type="submit" class="zammad-chat-button zammad-chat-send"<%= " style='background: #{ @background }'" if @background %>><%- @T('Send') %></button>
|
<button type="submit" class="zammad-chat-button zammad-chat-send"<%= " style='background: #{ @background }'" if @background %>><%- @T('Send') %></button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
12
public/assets/images/icons/office365-button.svg
Normal file
12
public/assets/images/icons/office365-button.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="29px" height="24px" viewBox="0 0 29 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 45.2 (43514) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>office365-button</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="office365-button" fill-rule="nonzero" fill="#FFFFFF">
|
||||||
|
<polyline id="office365" points="23 20.4918622 22.9835112 20.4918622 22.9835112 3.5578176 16.9485936 1.80999994 6.03297769 5.8992337 6 5.91572254 6 18.1504461 9.72647915 16.6994277 9.72647915 6.74016483 16.9485936 5.00883602 16.9156159 19.6509311 6 18.1504461 16.9156159 22.1902133 22.9835112 20.5083511 22.9835112 20.4918622"></polyline>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 890 B |
|
@ -126,7 +126,23 @@ class UserOrganizationControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_response(422)
|
assert_response(422)
|
||||||
result = JSON.parse(@response.body)
|
result = JSON.parse(@response.body)
|
||||||
assert(result['error'])
|
assert(result['error'])
|
||||||
assert_equal('User already exists!', result['error'])
|
assert_equal('Email address is already used for other user.', result['error'])
|
||||||
|
|
||||||
|
# email missing with enabled feature
|
||||||
|
params = { firstname: 'some firstname', signup: true }
|
||||||
|
post '/api/v1/users', params.to_json, headers
|
||||||
|
assert_response(422)
|
||||||
|
result = JSON.parse(@response.body)
|
||||||
|
assert(result['error'])
|
||||||
|
assert_equal('Attribute \'email\' required!', result['error'])
|
||||||
|
|
||||||
|
# email missing with enabled feature
|
||||||
|
params = { firstname: 'some firstname', signup: true }
|
||||||
|
post '/api/v1/users', params.to_json, headers
|
||||||
|
assert_response(422)
|
||||||
|
result = JSON.parse(@response.body)
|
||||||
|
assert(result['error'])
|
||||||
|
assert_equal('Attribute \'email\' required!', result['error'])
|
||||||
|
|
||||||
# create user with enabled feature (take customer role)
|
# create user with enabled feature (take customer role)
|
||||||
params = { firstname: 'Me First', lastname: 'Me Last', email: 'new_here@example.com', signup: true }
|
params = { firstname: 'Me First', lastname: 'Me Last', email: 'new_here@example.com', signup: true }
|
||||||
|
@ -322,7 +338,7 @@ class UserOrganizationControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_response(422)
|
assert_response(422)
|
||||||
result = JSON.parse(@response.body)
|
result = JSON.parse(@response.body)
|
||||||
assert(result)
|
assert(result)
|
||||||
assert_equal('User already exists!', result['error'])
|
assert_equal('Email address is already used for other user.', result['error'])
|
||||||
|
|
||||||
# missing required attributes
|
# missing required attributes
|
||||||
params = { note: 'some note' }
|
params = { note: 'some note' }
|
||||||
|
@ -330,15 +346,9 @@ class UserOrganizationControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_response(422)
|
assert_response(422)
|
||||||
result = JSON.parse(@response.body)
|
result = JSON.parse(@response.body)
|
||||||
assert(result)
|
assert(result)
|
||||||
assert_equal('Attribute \'login\' required!', result['error'])
|
assert_equal('Minimum one identifier (login, firstname, lastname, phone or email) for user is required.', result['error'])
|
||||||
|
|
||||||
params = { firstname: 'newfirstname123', note: 'some note' }
|
|
||||||
post '/api/v1/users', params.to_json, @headers.merge('Authorization' => credentials)
|
|
||||||
assert_response(422)
|
|
||||||
result = JSON.parse(@response.body)
|
|
||||||
assert(result)
|
|
||||||
assert_equal('Attribute \'login\' required!', result['error'])
|
|
||||||
|
|
||||||
|
# invalid email
|
||||||
params = { firstname: 'newfirstname123', email: 'some_what', note: 'some note' }
|
params = { firstname: 'newfirstname123', email: 'some_what', note: 'some note' }
|
||||||
post '/api/v1/users', params.to_json, @headers.merge('Authorization' => credentials)
|
post '/api/v1/users', params.to_json, @headers.merge('Authorization' => credentials)
|
||||||
assert_response(422)
|
assert_response(422)
|
||||||
|
@ -346,6 +356,20 @@ class UserOrganizationControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert(result)
|
assert(result)
|
||||||
assert_equal('Invalid email', result['error'])
|
assert_equal('Invalid email', result['error'])
|
||||||
|
|
||||||
|
# with valid attributes
|
||||||
|
params = { firstname: 'newfirstname123', note: 'some note' }
|
||||||
|
post '/api/v1/users', params.to_json, @headers.merge('Authorization' => credentials)
|
||||||
|
assert_response(201)
|
||||||
|
result = JSON.parse(@response.body)
|
||||||
|
assert(result)
|
||||||
|
user = User.find(result['id'])
|
||||||
|
assert_not(user.role?('Admin'))
|
||||||
|
assert_not(user.role?('Agent'))
|
||||||
|
assert(user.role?('Customer'))
|
||||||
|
assert(result['login'].start_with?('auto-'))
|
||||||
|
assert_equal('', result['email'])
|
||||||
|
assert_equal('newfirstname123', result['firstname'])
|
||||||
|
assert_equal('', result['lastname'])
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'user index and create with agent' do
|
test 'user index and create with agent' do
|
||||||
|
|
|
@ -4,16 +4,16 @@ require 'test_helper'
|
||||||
class EmailDeliverTest < ActiveSupport::TestCase
|
class EmailDeliverTest < ActiveSupport::TestCase
|
||||||
test 'basic check' do
|
test 'basic check' do
|
||||||
|
|
||||||
if !ENV['MAIL_SERVER']
|
if ENV['MAIL_SERVER'].blank?
|
||||||
raise "Need MAIL_SERVER as ENV variable like export MAIL_SERVER='mx.example.com'"
|
raise "Need MAIL_SERVER as ENV variable like export MAIL_SERVER='mx.example.com'"
|
||||||
end
|
end
|
||||||
if !ENV['MAIL_SERVER_ACCOUNT']
|
if ENV['MAIL_SERVER_ACCOUNT'].blank?
|
||||||
raise "Need MAIL_SERVER_ACCOUNT as ENV variable like export MAIL_SERVER_ACCOUNT='user:somepass'"
|
raise "Need MAIL_SERVER_ACCOUNT as ENV variable like export MAIL_SERVER_ACCOUNT='user:somepass'"
|
||||||
end
|
end
|
||||||
server_login = ENV['MAIL_SERVER_ACCOUNT'].split(':')[0]
|
server_login = ENV['MAIL_SERVER_ACCOUNT'].split(':')[0]
|
||||||
server_password = ENV['MAIL_SERVER_ACCOUNT'].split(':')[1]
|
server_password = ENV['MAIL_SERVER_ACCOUNT'].split(':')[1]
|
||||||
|
|
||||||
email_address = EmailAddress.create(
|
email_address = EmailAddress.create!(
|
||||||
realname: 'me Helpdesk',
|
realname: 'me Helpdesk',
|
||||||
email: "me#{rand(999_999_999)}@example.com",
|
email: "me#{rand(999_999_999)}@example.com",
|
||||||
updated_by_id: 1,
|
updated_by_id: 1,
|
||||||
|
@ -27,7 +27,7 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
||||||
created_by_id: 1,
|
created_by_id: 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
channel = Channel.create(
|
channel = Channel.create!(
|
||||||
area: 'Email::Account',
|
area: 'Email::Account',
|
||||||
group_id: group.id,
|
group_id: group.id,
|
||||||
options: {
|
options: {
|
||||||
|
@ -50,9 +50,9 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
||||||
)
|
)
|
||||||
|
|
||||||
email_address.channel_id = channel.id
|
email_address.channel_id = channel.id
|
||||||
email_address.save
|
email_address.save!
|
||||||
|
|
||||||
ticket1 = Ticket.create(
|
ticket1 = Ticket.create!(
|
||||||
title: 'some delivery test',
|
title: 'some delivery test',
|
||||||
group: group,
|
group: group,
|
||||||
customer_id: 2,
|
customer_id: 2,
|
||||||
|
@ -63,7 +63,7 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
||||||
)
|
)
|
||||||
assert(ticket1, 'ticket created')
|
assert(ticket1, 'ticket created')
|
||||||
|
|
||||||
article1 = Ticket::Article.create(
|
article1 = Ticket::Article.create!(
|
||||||
ticket_id: ticket1.id,
|
ticket_id: ticket1.id,
|
||||||
to: 'some_recipient@example_not_existing_what_ever.com',
|
to: 'some_recipient@example_not_existing_what_ever.com',
|
||||||
subject: 'some subject',
|
subject: 'some subject',
|
||||||
|
@ -189,7 +189,7 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
||||||
# remove background jobs
|
# remove background jobs
|
||||||
Delayed::Job.destroy_all
|
Delayed::Job.destroy_all
|
||||||
|
|
||||||
article2 = Ticket::Article.create(
|
article2 = Ticket::Article.create!(
|
||||||
ticket_id: ticket1.id,
|
ticket_id: ticket1.id,
|
||||||
to: 'some_recipient@example_not_existing_what_ever.com',
|
to: 'some_recipient@example_not_existing_what_ever.com',
|
||||||
subject: 'some subject2',
|
subject: 'some subject2',
|
||||||
|
|
232
test/integration/email_keep_on_server_test.rb
Normal file
232
test/integration/email_keep_on_server_test.rb
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
require 'test_helper'
|
||||||
|
require 'net/imap'
|
||||||
|
|
||||||
|
class EmailKeepOnServerTest < ActiveSupport::TestCase
|
||||||
|
setup do
|
||||||
|
|
||||||
|
if ENV['KEEP_ON_MAIL_SERVER'].blank?
|
||||||
|
raise "Need KEEP_ON_MAIL_SERVER as ENV variable like export KEEP_ON_MAIL_SERVER='mx.example.com'"
|
||||||
|
end
|
||||||
|
if ENV['KEEP_ON_MAIL_SERVER_ACCOUNT'].blank?
|
||||||
|
raise "Need KEEP_ON_MAIL_SERVER_ACCOUNT as ENV variable like export KEEP_ON_MAIL_SERVER_ACCOUNT='user:somepass'"
|
||||||
|
end
|
||||||
|
@server_login = ENV['KEEP_ON_MAIL_SERVER_ACCOUNT'].split(':')[0]
|
||||||
|
@server_password = ENV['KEEP_ON_MAIL_SERVER_ACCOUNT'].split(':')[1]
|
||||||
|
|
||||||
|
@folder = "keep_on_mail_server_#{rand(999_999_999)}"
|
||||||
|
|
||||||
|
email_address = EmailAddress.create!(
|
||||||
|
realname: 'me Helpdesk',
|
||||||
|
email: "me#{rand(999_999_999)}@example.com",
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
group = Group.create_or_update(
|
||||||
|
name: 'KeepOnServerTest',
|
||||||
|
email_address_id: email_address.id,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@channel = Channel.create!(
|
||||||
|
area: 'Email::Account',
|
||||||
|
group_id: group.id,
|
||||||
|
options: {
|
||||||
|
inbound: {
|
||||||
|
adapter: 'imap',
|
||||||
|
options: {
|
||||||
|
host: ENV['KEEP_ON_MAIL_SERVER'],
|
||||||
|
user: @server_login,
|
||||||
|
password: @server_password,
|
||||||
|
ssl: true,
|
||||||
|
folder: @folder,
|
||||||
|
#keep_on_server: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
outbound: {
|
||||||
|
adapter: 'sendmail'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
active: true,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
email_address.channel_id = @channel.id
|
||||||
|
email_address.save!
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'keep on server' do
|
||||||
|
@channel.options[:inbound][:options][:keep_on_server] = true
|
||||||
|
@channel.save!
|
||||||
|
|
||||||
|
# clean mailbox
|
||||||
|
imap = Net::IMAP.new(ENV['KEEP_ON_MAIL_SERVER'], 993, true, nil, false)
|
||||||
|
imap.login(@server_login, @server_password)
|
||||||
|
imap.create(@folder)
|
||||||
|
imap.select(@folder)
|
||||||
|
|
||||||
|
# put unseen message in it
|
||||||
|
imap.append(@folder, "Subject: hello1
|
||||||
|
From: shugo@example.com
|
||||||
|
To: shugo@example.com
|
||||||
|
Message-ID: <some1@example_keep_on_server>
|
||||||
|
|
||||||
|
hello world
|
||||||
|
".gsub(/\n/, "\r\n"), [], Time.zone.now)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
message_meta = imap.fetch(1, ['FLAGS'])[0].attr
|
||||||
|
assert_not(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
|
||||||
|
# fetch messages
|
||||||
|
article_count = Ticket::Article.count
|
||||||
|
@channel.fetch(true)
|
||||||
|
assert_equal(article_count + 1, Ticket::Article.count)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
message_meta = imap.fetch(1, ['RFC822.HEADER', 'FLAGS'])[0].attr
|
||||||
|
assert(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
|
||||||
|
# fetch messages
|
||||||
|
article_count = Ticket::Article.count
|
||||||
|
@channel.fetch(true)
|
||||||
|
assert_equal(article_count, Ticket::Article.count)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
# put unseen message in it
|
||||||
|
imap.append(@folder, "Subject: hello2
|
||||||
|
From: shugo@example.com
|
||||||
|
To: shugo@example.com
|
||||||
|
Message-ID: <some2@example_keep_on_server>
|
||||||
|
|
||||||
|
hello world
|
||||||
|
".gsub(/\n/, "\r\n"), [], Time.zone.now)
|
||||||
|
|
||||||
|
message_meta = imap.fetch(1, ['FLAGS'])[0].attr
|
||||||
|
assert(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
message_meta = imap.fetch(2, ['FLAGS'])[0].attr
|
||||||
|
assert_not(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
|
||||||
|
# fetch messages
|
||||||
|
article_count = Ticket::Article.count
|
||||||
|
@channel.fetch(true)
|
||||||
|
assert_equal(article_count + 1, Ticket::Article.count)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(2, message_ids.count)
|
||||||
|
|
||||||
|
message_meta = imap.fetch(1, ['FLAGS'])[0].attr
|
||||||
|
assert(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
message_meta = imap.fetch(2, ['FLAGS'])[0].attr
|
||||||
|
assert(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
|
||||||
|
# set messages to not seen
|
||||||
|
imap.store(1, '-FLAGS', [:Seen])
|
||||||
|
imap.store(2, '-FLAGS', [:Seen])
|
||||||
|
|
||||||
|
# fetch messages
|
||||||
|
article_count = Ticket::Article.count
|
||||||
|
@channel.fetch(true)
|
||||||
|
assert_equal(article_count, Ticket::Article.count)
|
||||||
|
|
||||||
|
imap.delete(@folder)
|
||||||
|
@channel.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'keep not on server' do
|
||||||
|
@channel.options[:inbound][:options][:keep_on_server] = false
|
||||||
|
@channel.save!
|
||||||
|
|
||||||
|
# clean mailbox
|
||||||
|
imap = Net::IMAP.new(ENV['KEEP_ON_MAIL_SERVER'], 993, true, nil, false)
|
||||||
|
imap.login(@server_login, @server_password)
|
||||||
|
imap.create(@folder)
|
||||||
|
imap.select(@folder)
|
||||||
|
|
||||||
|
# put unseen message in it
|
||||||
|
imap.append(@folder, "Subject: hello1
|
||||||
|
From: shugo@example.com
|
||||||
|
To: shugo@example.com
|
||||||
|
Message-ID: <some1@example_remove_from_server>
|
||||||
|
|
||||||
|
hello world
|
||||||
|
".gsub(/\n/, "\r\n"), [], Time.zone.now)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
message_meta = imap.fetch(1, ['FLAGS'])[0].attr
|
||||||
|
assert_not(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
|
||||||
|
# fetch messages
|
||||||
|
article_count = Ticket::Article.count
|
||||||
|
@channel.fetch(true)
|
||||||
|
assert_equal(article_count + 1, Ticket::Article.count)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
# put unseen message in it
|
||||||
|
imap.append(@folder, "Subject: hello2
|
||||||
|
From: shugo@example.com
|
||||||
|
To: shugo@example.com
|
||||||
|
Message-ID: <some2@example_remove_from_server>
|
||||||
|
|
||||||
|
hello world
|
||||||
|
".gsub(/\n/, "\r\n"), [], Time.zone.now)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
message_meta = imap.fetch(1, ['FLAGS'])[0].attr
|
||||||
|
assert_not(message_meta['FLAGS'].include?(:Seen))
|
||||||
|
|
||||||
|
# fetch messages
|
||||||
|
article_count = Ticket::Article.count
|
||||||
|
@channel.fetch(true)
|
||||||
|
assert_equal(article_count + 1, Ticket::Article.count)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
# put unseen message in it
|
||||||
|
imap.append(@folder, "Subject: hello2
|
||||||
|
From: shugo@example.com
|
||||||
|
To: shugo@example.com
|
||||||
|
Message-ID: <some2@example_remove_from_server>
|
||||||
|
|
||||||
|
hello world
|
||||||
|
".gsub(/\n/, "\r\n"), [], Time.zone.now)
|
||||||
|
|
||||||
|
# verify if message is still on server
|
||||||
|
message_ids = imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||||
|
assert_equal(1, message_ids.count)
|
||||||
|
|
||||||
|
# fetch messages
|
||||||
|
article_count = Ticket::Article.count
|
||||||
|
@channel.fetch(true)
|
||||||
|
assert_equal(article_count + 1, Ticket::Article.count)
|
||||||
|
|
||||||
|
imap.delete(@folder)
|
||||||
|
@channel.destroy!
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -51,7 +51,7 @@ class GeoLocationTest < ActiveSupport::TestCase
|
||||||
login: 'some_geo_login2',
|
login: 'some_geo_login2',
|
||||||
firstname: 'First',
|
firstname: 'First',
|
||||||
lastname: 'Last',
|
lastname: 'Last',
|
||||||
email: 'some_geo_login1@example.com',
|
email: 'some_geo_login2@example.com',
|
||||||
password: 'test',
|
password: 'test',
|
||||||
street: 'Marienstrasse 13',
|
street: 'Marienstrasse 13',
|
||||||
city: 'Berlin',
|
city: 'Berlin',
|
||||||
|
|
|
@ -10,7 +10,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
||||||
login: 'admin',
|
login: 'admin',
|
||||||
firstname: 'Bob',
|
firstname: 'Bob',
|
||||||
lastname: 'Smith',
|
lastname: 'Smith',
|
||||||
email: 'bob@example.com',
|
email: 'bob+active_stream@example.com',
|
||||||
password: 'some_pass',
|
password: 'some_pass',
|
||||||
active: true,
|
active: true,
|
||||||
roles: roles,
|
roles: roles,
|
||||||
|
@ -23,7 +23,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'ticket+user' do
|
test 'ticket+user' do
|
||||||
ticket = Ticket.create(
|
ticket = Ticket.create!(
|
||||||
group_id: Group.lookup(name: 'Users').id,
|
group_id: Group.lookup(name: 'Users').id,
|
||||||
customer_id: @current_user.id,
|
customer_id: @current_user.id,
|
||||||
owner_id: User.lookup(login: '-').id,
|
owner_id: User.lookup(login: '-').id,
|
||||||
|
@ -35,7 +35,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
||||||
)
|
)
|
||||||
travel 2.seconds
|
travel 2.seconds
|
||||||
|
|
||||||
article = Ticket::Article.create(
|
article = Ticket::Article.create!(
|
||||||
ticket_id: ticket.id,
|
ticket_id: ticket.id,
|
||||||
updated_by_id: @current_user.id,
|
updated_by_id: @current_user.id,
|
||||||
created_by_id: @current_user.id,
|
created_by_id: @current_user.id,
|
||||||
|
@ -86,12 +86,12 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
||||||
assert(stream.empty?)
|
assert(stream.empty?)
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
ticket.destroy
|
ticket.destroy!
|
||||||
travel_back
|
travel_back
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'organization' do
|
test 'organization' do
|
||||||
organization = Organization.create(
|
organization = Organization.create!(
|
||||||
name: 'some name',
|
name: 'some name',
|
||||||
updated_by_id: @current_user.id,
|
updated_by_id: @current_user.id,
|
||||||
created_by_id: @current_user.id,
|
created_by_id: @current_user.id,
|
||||||
|
@ -125,12 +125,12 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
||||||
assert(stream.empty?)
|
assert(stream.empty?)
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
organization.destroy
|
organization.destroy!
|
||||||
travel_back
|
travel_back
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'user with update check false' do
|
test 'user with update check false' do
|
||||||
user = User.create(
|
user = User.create!(
|
||||||
login: 'someemail@example.com',
|
login: 'someemail@example.com',
|
||||||
email: 'someemail@example.com',
|
email: 'someemail@example.com',
|
||||||
firstname: 'Bob Smith II',
|
firstname: 'Bob Smith II',
|
||||||
|
@ -157,12 +157,12 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
||||||
assert(stream.empty?)
|
assert(stream.empty?)
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
user.destroy
|
user.destroy!
|
||||||
travel_back
|
travel_back
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'user with update check true' do
|
test 'user with update check true' do
|
||||||
user = User.create(
|
user = User.create!(
|
||||||
login: 'someemail@example.com',
|
login: 'someemail@example.com',
|
||||||
email: 'someemail@example.com',
|
email: 'someemail@example.com',
|
||||||
firstname: 'Bob Smith II',
|
firstname: 'Bob Smith II',
|
||||||
|
@ -204,7 +204,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
||||||
assert(stream.empty?)
|
assert(stream.empty?)
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
user.destroy
|
user.destroy!
|
||||||
travel_back
|
travel_back
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,12 @@ class EmailBuildTest < ActiveSupport::TestCase
|
||||||
assert(result !~ /font-family/, 'test 2')
|
assert(result !~ /font-family/, 'test 2')
|
||||||
assert(result =~ %r{<b>test</b>}, 'test 2')
|
assert(result =~ %r{<b>test</b>}, 'test 2')
|
||||||
|
|
||||||
|
# Issue #1230, missing backslashes
|
||||||
|
# 'Test URL: \\storage\project\100242-Inc'
|
||||||
|
html = '<b>Test URL</b>: \\\\storage\\project\\100242-Inc'
|
||||||
|
result = Channel::EmailBuild.html_complete_check(html)
|
||||||
|
assert(result.include?(html), 'backslashes must be kept')
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'html email + attachment check' do
|
test 'html email + attachment check' do
|
||||||
|
|
|
@ -155,12 +155,12 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
# use transaction
|
# use transaction
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
ticket = Ticket.create(test[:ticket_create][:ticket])
|
ticket = Ticket.create!(test[:ticket_create][:ticket])
|
||||||
test[:ticket_create][:article][:ticket_id] = ticket.id
|
test[:ticket_create][:article][:ticket_id] = ticket.id
|
||||||
article = Ticket::Article.create(test[:ticket_create][:article])
|
article = Ticket::Article.create!(test[:ticket_create][:article])
|
||||||
|
|
||||||
assert_equal(ticket.class.to_s, 'Ticket')
|
assert_equal(ticket.class, Ticket)
|
||||||
assert_equal(article.class.to_s, 'Ticket::Article')
|
assert_equal(article.class, Ticket::Article)
|
||||||
|
|
||||||
# update ticket
|
# update ticket
|
||||||
if test[:ticket_update][:ticket]
|
if test[:ticket_update][:ticket]
|
||||||
|
@ -185,25 +185,21 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
# delete tickets
|
# delete tickets
|
||||||
tickets.each { |ticket|
|
tickets.each(&:destroy!)
|
||||||
ticket_id = ticket.id
|
|
||||||
ticket.destroy
|
|
||||||
found = Ticket.where(id: ticket_id).first
|
|
||||||
assert_not(found, 'Ticket destroyed')
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'user' do
|
test 'user' do
|
||||||
|
name = rand(999_999)
|
||||||
tests = [
|
tests = [
|
||||||
|
|
||||||
# test 1
|
# test 1
|
||||||
{
|
{
|
||||||
user_create: {
|
user_create: {
|
||||||
user: {
|
user: {
|
||||||
login: 'some_login_test',
|
login: "some_login_test-#{name}",
|
||||||
firstname: 'Bob',
|
firstname: 'Bob',
|
||||||
lastname: 'Smith',
|
lastname: 'Smith',
|
||||||
email: 'somebody@example.com',
|
email: "somebody-#{name}@example.com",
|
||||||
active: true,
|
active: true,
|
||||||
updated_by_id: current_user.id,
|
updated_by_id: current_user.id,
|
||||||
created_by_id: current_user.id,
|
created_by_id: current_user.id,
|
||||||
|
@ -213,7 +209,7 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
user: {
|
user: {
|
||||||
firstname: 'Bob',
|
firstname: 'Bob',
|
||||||
lastname: 'Master',
|
lastname: 'Master',
|
||||||
email: 'master@example.com',
|
email: "master-#{name}@example.com",
|
||||||
active: false,
|
active: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -236,8 +232,8 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
history_object: 'User',
|
history_object: 'User',
|
||||||
history_type: 'updated',
|
history_type: 'updated',
|
||||||
history_attribute: 'email',
|
history_attribute: 'email',
|
||||||
value_from: 'somebody@example.com',
|
value_from: "somebody-#{name}@example.com",
|
||||||
value_to: 'master@example.com',
|
value_to: "master-#{name}@example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
result: true,
|
result: true,
|
||||||
|
@ -258,9 +254,8 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
# user transaction
|
# user transaction
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
user = User.create(test[:user_create][:user])
|
user = User.create!(test[:user_create][:user])
|
||||||
|
assert_equal(user.class, User)
|
||||||
assert_equal(user.class.to_s, 'User')
|
|
||||||
|
|
||||||
# update user
|
# update user
|
||||||
if test[:user_update][:user]
|
if test[:user_update][:user]
|
||||||
|
@ -277,12 +272,7 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
# delete user
|
# delete user
|
||||||
users.each { |user|
|
users.each(&:destroy!)
|
||||||
user_id = user.id
|
|
||||||
user.destroy
|
|
||||||
found = User.where(id: user_id).first
|
|
||||||
assert_not(found, 'User destroyed')
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'organization' do
|
test 'organization' do
|
||||||
|
@ -328,9 +318,8 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
# user transaction
|
# user transaction
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
organization = Organization.create(test[:organization_create][:organization])
|
organization = Organization.create!(test[:organization_create][:organization])
|
||||||
|
assert_equal(organization.class, Organization)
|
||||||
assert_equal(organization.class.to_s, 'Organization')
|
|
||||||
|
|
||||||
# update organization
|
# update organization
|
||||||
if test[:organization_update][:organization]
|
if test[:organization_update][:organization]
|
||||||
|
@ -346,12 +335,7 @@ class HistoryTest < ActiveSupport::TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
# delete user
|
# delete user
|
||||||
organizations.each { |organization|
|
organizations.each(&:destroy!)
|
||||||
organization_id = organization.id
|
|
||||||
organization.destroy
|
|
||||||
found = Organization.where(id: organization_id).first
|
|
||||||
assert_not(found, 'Organization destroyed')
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def history_check(history_list, history_check)
|
def history_check(history_list, history_check)
|
||||||
|
|
215
test/unit/overview_test.rb
Normal file
215
test/unit/overview_test.rb
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class OverviewTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
|
test 'overview link' do
|
||||||
|
UserInfo.current_user_id = 1
|
||||||
|
overview = Overview.create!(
|
||||||
|
name: 'Not Shown Admin 2',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_equal(overview.link, 'not_shown_admin_2')
|
||||||
|
overview.destroy!
|
||||||
|
|
||||||
|
overview = Overview.create!(
|
||||||
|
name: 'My assigned Tickets',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_equal(overview.link, 'my_assigned_tickets')
|
||||||
|
overview.destroy!
|
||||||
|
|
||||||
|
overview = Overview.create!(
|
||||||
|
name: 'Übersicht',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_equal(overview.link, 'ubersicht')
|
||||||
|
overview.destroy!
|
||||||
|
|
||||||
|
overview = Overview.create!(
|
||||||
|
name: " Übersicht \n",
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_equal(overview.link, 'ubersicht')
|
||||||
|
overview.destroy!
|
||||||
|
|
||||||
|
overview1 = Overview.create!(
|
||||||
|
name: 'Meine Übersicht',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_equal(overview1.link, 'meine_ubersicht')
|
||||||
|
overview2 = Overview.create!(
|
||||||
|
name: 'Meine Übersicht',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert(overview2.link.start_with?('meine_ubersicht'))
|
||||||
|
assert_not_equal(overview1.link, overview2.link)
|
||||||
|
overview1.destroy!
|
||||||
|
overview2.destroy!
|
||||||
|
|
||||||
|
overview = Overview.create!(
|
||||||
|
name: 'Д дФ ф',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_match(/^\d{1,3}$/, overview.link)
|
||||||
|
overview.destroy!
|
||||||
|
|
||||||
|
overview = Overview.create!(
|
||||||
|
name: ' Д дФ ф abc ',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_equal(overview.link, 'abc')
|
||||||
|
overview.destroy!
|
||||||
|
|
||||||
|
overview = Overview.create!(
|
||||||
|
name: 'Übersicht',
|
||||||
|
link: 'my_overview',
|
||||||
|
condition: {
|
||||||
|
'ticket.state_id' => {
|
||||||
|
operator: 'is',
|
||||||
|
value: [1, 2, 3],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
by: 'created_at',
|
||||||
|
direction: 'DESC',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
d: %w(title customer state created_at),
|
||||||
|
s: %w(number title customer state created_at),
|
||||||
|
m: %w(number title customer state created_at),
|
||||||
|
view_mode_default: 's',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert_equal(overview.link, 'my_overview')
|
||||||
|
|
||||||
|
overview.name = 'Übersicht2'
|
||||||
|
overview.link = 'my_overview2'
|
||||||
|
overview.save!
|
||||||
|
|
||||||
|
assert_equal(overview.link, 'my_overview2')
|
||||||
|
|
||||||
|
overview.destroy!
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
34
test/unit/ticket_null_byte_test.rb
Normal file
34
test/unit/ticket_null_byte_test.rb
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class TicketNullByteTest < ActiveSupport::TestCase
|
||||||
|
test 'null byte test' do
|
||||||
|
ticket1 = Ticket.create!(
|
||||||
|
title: "some title \u0000 123",
|
||||||
|
group: Group.lookup(name: 'Users'),
|
||||||
|
customer_id: 2,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
assert(ticket1, 'ticket created')
|
||||||
|
|
||||||
|
article1 = Ticket::Article.create!(
|
||||||
|
ticket_id: ticket1.id,
|
||||||
|
from: 'some_customer_com-1@example.com',
|
||||||
|
to: 'some_zammad_com-1@example.com',
|
||||||
|
subject: "com test 1\u0000",
|
||||||
|
message_id: 'some@id_com_1',
|
||||||
|
body: "some\u0000message 123",
|
||||||
|
internal: false,
|
||||||
|
sender: Ticket::Article::Sender.find_by(name: 'Customer'),
|
||||||
|
type: Ticket::Article::Type.find_by(name: 'email'),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
assert(article1, 'ticket created')
|
||||||
|
|
||||||
|
ticket1.destroy!
|
||||||
|
article1.destroy!
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
|
@ -302,7 +302,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'bbb overiview index' do
|
test 'bbb overview index' do
|
||||||
|
|
||||||
result = Ticket::Overviews.all(
|
result = Ticket::Overviews.all(
|
||||||
current_user: @agent1,
|
current_user: @agent1,
|
||||||
|
@ -343,7 +343,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'ccc overiview content' do
|
test 'ccc overview content' do
|
||||||
|
|
||||||
Ticket.destroy_all
|
Ticket.destroy_all
|
||||||
|
|
||||||
|
|
|
@ -245,9 +245,9 @@ class UserTest < ActiveSupport::TestCase
|
||||||
tests.each { |test|
|
tests.each { |test|
|
||||||
|
|
||||||
# check if user exists
|
# check if user exists
|
||||||
user = User.where( login: test[:create][:login] ).first
|
user = User.where(login: test[:create][:login]).first
|
||||||
if user
|
if user
|
||||||
user.destroy
|
user.destroy!
|
||||||
end
|
end
|
||||||
|
|
||||||
user = User.create( test[:create] )
|
user = User.create( test[:create] )
|
||||||
|
@ -266,8 +266,8 @@ class UserTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
if test[:create_verify][:image_md5]
|
if test[:create_verify][:image_md5]
|
||||||
file = Avatar.get_by_hash( user.image )
|
file = Avatar.get_by_hash(user.image)
|
||||||
file_md5 = Digest::MD5.hexdigest( file.content )
|
file_md5 = Digest::MD5.hexdigest(file.content)
|
||||||
assert_equal(test[:create_verify][:image_md5], file_md5, "create avatar md5 check in (#{test[:name]})")
|
assert_equal(test[:create_verify][:image_md5], file_md5, "create avatar md5 check in (#{test[:name]})")
|
||||||
end
|
end
|
||||||
if test[:update]
|
if test[:update]
|
||||||
|
@ -275,7 +275,7 @@ class UserTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
test[:update_verify].each { |key, value|
|
test[:update_verify].each { |key, value|
|
||||||
next if key == :image_md5
|
next if key == :image_md5
|
||||||
if user.respond_to?( key )
|
if user.respond_to?(key)
|
||||||
assert_equal(value, user.send(key), "update check #{key} in (#{test[:name]})")
|
assert_equal(value, user.send(key), "update check #{key} in (#{test[:name]})")
|
||||||
else
|
else
|
||||||
assert_equal(value, user[key], "update check #{key} in (#{test[:name]})")
|
assert_equal(value, user[key], "update check #{key} in (#{test[:name]})")
|
||||||
|
@ -289,10 +289,252 @@ class UserTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
user.destroy
|
user.destroy!
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test 'without email - but login eq email' do
|
||||||
|
name = rand(999_999_999)
|
||||||
|
|
||||||
|
login = "admin-role_without_email#{name}@example.com"
|
||||||
|
email = "admin-role_without_email#{name}@example.com"
|
||||||
|
admin = User.create_or_update(
|
||||||
|
login: login,
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin#{name}",
|
||||||
|
#email: "",
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(admin.id)
|
||||||
|
assert_equal(admin.login, login)
|
||||||
|
assert_equal(admin.email, '')
|
||||||
|
|
||||||
|
admin.email = email
|
||||||
|
admin.save!
|
||||||
|
|
||||||
|
assert_equal(admin.login, login)
|
||||||
|
assert_equal(admin.email, email)
|
||||||
|
|
||||||
|
admin.email = ''
|
||||||
|
admin.save!
|
||||||
|
|
||||||
|
assert(admin.id)
|
||||||
|
assert(admin.login)
|
||||||
|
assert_not_equal(admin.login, login)
|
||||||
|
assert_equal(admin.email, '')
|
||||||
|
|
||||||
|
admin.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'without email - but login ne email' do
|
||||||
|
name = rand(999_999_999)
|
||||||
|
|
||||||
|
login = "admin-role_without_email#{name}"
|
||||||
|
email = "admin-role_without_email#{name}@example.com"
|
||||||
|
admin = User.create_or_update(
|
||||||
|
login: login,
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin#{name}",
|
||||||
|
#email: "",
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(admin.id)
|
||||||
|
assert_equal(admin.login, login)
|
||||||
|
assert_equal(admin.email, '')
|
||||||
|
|
||||||
|
admin.email = email
|
||||||
|
admin.save!
|
||||||
|
|
||||||
|
assert_equal(admin.login, login)
|
||||||
|
assert_equal(admin.email, email)
|
||||||
|
|
||||||
|
admin.email = ''
|
||||||
|
admin.save!
|
||||||
|
|
||||||
|
assert(admin.id)
|
||||||
|
assert_equal(admin.login, login)
|
||||||
|
assert_equal(admin.email, '')
|
||||||
|
|
||||||
|
admin.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'uniq email' do
|
||||||
|
name = rand(999_999_999)
|
||||||
|
|
||||||
|
email1 = "admin1-role_without_email#{name}@example.com"
|
||||||
|
admin1 = User.create!(
|
||||||
|
login: email1,
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin1#{name}",
|
||||||
|
email: email1,
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(admin1.id)
|
||||||
|
assert_equal(admin1.email, email1)
|
||||||
|
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
User.create!(
|
||||||
|
login: "#{email1}-1",
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin1#{name}",
|
||||||
|
email: email1,
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
email2 = "admin2-role_without_email#{name}@example.com"
|
||||||
|
admin2 = User.create!(
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin2#{name}",
|
||||||
|
email: email2,
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_raises(Exceptions::UnprocessableEntity) {
|
||||||
|
admin2.email = email1
|
||||||
|
admin2.save!
|
||||||
|
}
|
||||||
|
|
||||||
|
admin1.email = admin1.email
|
||||||
|
admin1.save!
|
||||||
|
|
||||||
|
admin2.destroy!
|
||||||
|
admin1.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'uniq email - multiple use' do
|
||||||
|
Setting.set('user_email_multiple_use', true)
|
||||||
|
name = rand(999_999_999)
|
||||||
|
|
||||||
|
email1 = "admin1-role_without_email#{name}@example.com"
|
||||||
|
admin1 = User.create!(
|
||||||
|
login: email1,
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin1#{name}",
|
||||||
|
email: email1,
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(admin1.id)
|
||||||
|
assert_equal(admin1.email, email1)
|
||||||
|
|
||||||
|
admin2 = User.create!(
|
||||||
|
login: "#{email1}-1",
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin1#{name}",
|
||||||
|
email: email1,
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
assert_equal(admin2.email, email1)
|
||||||
|
admin2.destroy!
|
||||||
|
admin1.destroy!
|
||||||
|
Setting.set('user_email_multiple_use', false)
|
||||||
|
end
|
||||||
|
|
||||||
|
test 'ensure roles' do
|
||||||
|
name = rand(999_999_999)
|
||||||
|
|
||||||
|
admin = User.create_or_update(
|
||||||
|
login: "admin-role#{name}@example.com",
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Admin#{name}",
|
||||||
|
email: "admin-role#{name}@example.com",
|
||||||
|
password: 'adminpw',
|
||||||
|
active: true,
|
||||||
|
roles: Role.where(name: %w(Admin Agent)),
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
customer1 = User.create_or_update(
|
||||||
|
login: "user-ensure-role1-#{name}@example.com",
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Customer#{name}",
|
||||||
|
email: "user-ensure-role1-#{name}@example.com",
|
||||||
|
password: 'customerpw',
|
||||||
|
active: true,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
assert_equal(customer1.role_ids.sort, Role.signup_role_ids)
|
||||||
|
|
||||||
|
roles = Role.where(name: 'Agent')
|
||||||
|
customer1.roles = roles
|
||||||
|
customer1.save!
|
||||||
|
|
||||||
|
assert_equal(customer1.role_ids.count, 1)
|
||||||
|
assert_equal(customer1.role_ids.first, roles.first.id)
|
||||||
|
assert_equal(customer1.roles.first.id, roles.first.id)
|
||||||
|
|
||||||
|
customer1.roles = []
|
||||||
|
customer1.save!
|
||||||
|
|
||||||
|
assert_equal(customer1.role_ids.sort, Role.signup_role_ids)
|
||||||
|
customer1.destroy!
|
||||||
|
|
||||||
|
customer2 = User.create_or_update(
|
||||||
|
login: "user-ensure-role2-#{name}@example.com",
|
||||||
|
firstname: 'Role',
|
||||||
|
lastname: "Customer#{name}",
|
||||||
|
email: "user-ensure-role2-#{name}@example.com",
|
||||||
|
password: 'customerpw',
|
||||||
|
roles: roles,
|
||||||
|
active: true,
|
||||||
|
updated_by_id: 1,
|
||||||
|
created_by_id: 1,
|
||||||
|
)
|
||||||
|
assert_equal(customer2.role_ids.count, 1)
|
||||||
|
assert_equal(customer2.role_ids.first, roles.first.id)
|
||||||
|
assert_equal(customer2.roles.first.id, roles.first.id)
|
||||||
|
|
||||||
|
roles = Role.where(name: 'Admin')
|
||||||
|
customer2.role_ids = [roles.first.id]
|
||||||
|
customer2.save!
|
||||||
|
|
||||||
|
assert_equal(customer2.role_ids.count, 1)
|
||||||
|
assert_equal(customer2.role_ids.first, roles.first.id)
|
||||||
|
assert_equal(customer2.roles.first.id, roles.first.id)
|
||||||
|
|
||||||
|
customer2.roles = []
|
||||||
|
customer2.save!
|
||||||
|
|
||||||
|
assert_equal(customer2.role_ids.sort, Role.signup_role_ids)
|
||||||
|
customer2.destroy!
|
||||||
|
|
||||||
|
admin.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
test 'user default preferences' do
|
test 'user default preferences' do
|
||||||
name = rand(999_999_999)
|
name = rand(999_999_999)
|
||||||
groups = Group.where(name: 'Users')
|
groups = Group.where(name: 'Users')
|
||||||
|
@ -352,7 +594,6 @@ class UserTest < ActiveSupport::TestCase
|
||||||
assert(customer1.preferences['notification_config'])
|
assert(customer1.preferences['notification_config'])
|
||||||
assert(customer1.preferences['notification_config']['matrix']['create'])
|
assert(customer1.preferences['notification_config']['matrix']['create'])
|
||||||
assert(customer1.preferences['notification_config']['matrix']['update'])
|
assert(customer1.preferences['notification_config']['matrix']['update'])
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test 'permission' do
|
test 'permission' do
|
||||||
|
@ -557,7 +798,7 @@ class UserTest < ActiveSupport::TestCase
|
||||||
# So we need to merge them with the User Nr 1 and destroy them afterwards
|
# So we need to merge them with the User Nr 1 and destroy them afterwards
|
||||||
User.with_permissions('admin').each do |user|
|
User.with_permissions('admin').each do |user|
|
||||||
Models.merge('User', 1, user.id)
|
Models.merge('User', 1, user.id)
|
||||||
user.destroy
|
user.destroy!
|
||||||
end
|
end
|
||||||
|
|
||||||
# store current admin count
|
# store current admin count
|
||||||
|
|
13
vendor/lib/microsoft_office365_database.rb
vendored
Normal file
13
vendor/lib/microsoft_office365_database.rb
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
class MicrosoftOffice365Database < OmniAuth::Strategies::MicrosoftOffice365
|
||||||
|
option :name, 'microsoft_office365'
|
||||||
|
|
||||||
|
def initialize(app, *args, &block)
|
||||||
|
|
||||||
|
# database lookup
|
||||||
|
config = Setting.get('auth_microsoft_office365_credentials') || {}
|
||||||
|
args[0] = config['app_id']
|
||||||
|
args[1] = config['app_secret']
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in a new issue