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
|
||||
- rake db:drop
|
||||
|
||||
test:integration:email_keep_on_server:
|
||||
stage: test
|
||||
tags:
|
||||
- core
|
||||
script:
|
||||
- export RAILS_ENV=test
|
||||
- rake db:create
|
||||
- rake db:migrate
|
||||
- ruby -I test/ test/integration/email_keep_on_server_test.rb
|
||||
- rake db:drop
|
||||
|
||||
test:integration:twitter:
|
||||
stage: test
|
||||
tags:
|
||||
|
|
|
@ -7,7 +7,6 @@ notifications:
|
|||
env:
|
||||
- DB=mysql
|
||||
- DB=postgresql
|
||||
- BUNDLE_JOBS=8
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
apt:
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -40,6 +40,7 @@ gem 'omniauth-gitlab'
|
|||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-linkedin-oauth2'
|
||||
gem 'omniauth-twitter'
|
||||
gem 'omniauth-microsoft-office365'
|
||||
|
||||
gem 'twitter'
|
||||
gem 'telegramAPI'
|
||||
|
|
35
Gemfile.lock
35
Gemfile.lock
|
@ -151,10 +151,11 @@ GEM
|
|||
guard (~> 2.8)
|
||||
guard-compat (~> 1.0)
|
||||
multi_json (~> 1.8)
|
||||
guard-symlink (0.1.0)
|
||||
guard-symlink (0.1.1)
|
||||
guard
|
||||
guard-compat (~> 1.1)
|
||||
hashdiff (0.3.2)
|
||||
hashie (3.4.4)
|
||||
hashie (3.5.5)
|
||||
htmlentities (4.3.4)
|
||||
http (1.0.4)
|
||||
addressable (~> 2.3)
|
||||
|
@ -169,7 +170,7 @@ GEM
|
|||
icalendar (2.4.1)
|
||||
inflection (1.0.0)
|
||||
json (1.8.6)
|
||||
jwt (1.5.4)
|
||||
jwt (1.5.6)
|
||||
kgio (2.11.0)
|
||||
koala (2.4.0)
|
||||
addressable
|
||||
|
@ -194,7 +195,7 @@ GEM
|
|||
mini_portile2 (2.2.0)
|
||||
minitest (5.10.2)
|
||||
multi_json (1.12.1)
|
||||
multi_xml (0.5.5)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mysql2 (0.4.6)
|
||||
naught (1.1.0)
|
||||
|
@ -208,33 +209,36 @@ GEM
|
|||
nenv (~> 0.1)
|
||||
shellany (~> 0.0)
|
||||
oauth (0.5.1)
|
||||
oauth2 (1.2.0)
|
||||
faraday (>= 0.8, < 0.10)
|
||||
oauth2 (1.4.0)
|
||||
faraday (>= 0.8, < 0.13)
|
||||
jwt (~> 1.0)
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
octokit (4.4.1)
|
||||
sawyer (~> 0.7.0, >= 0.5.3)
|
||||
omniauth (1.3.1)
|
||||
hashie (>= 1.2, < 4)
|
||||
rack (>= 1.0, < 3)
|
||||
omniauth (1.6.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
omniauth-facebook (4.0.0)
|
||||
omniauth-oauth2 (~> 1.2)
|
||||
omniauth-github (1.1.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.1)
|
||||
omniauth-github (1.3.0)
|
||||
omniauth (~> 1.5)
|
||||
omniauth-oauth2 (>= 1.4.0, < 2.0)
|
||||
omniauth-gitlab (1.0.2)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2 (~> 1.0)
|
||||
omniauth-google-oauth2 (0.4.1)
|
||||
jwt (~> 1.5.2)
|
||||
omniauth-google-oauth2 (0.5.0)
|
||||
jwt (~> 1.5)
|
||||
multi_json (~> 1.3)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.3.1)
|
||||
omniauth-linkedin-oauth2 (0.1.5)
|
||||
omniauth (~> 1.0)
|
||||
omniauth-oauth2
|
||||
omniauth-microsoft-office365 (0.0.7)
|
||||
omniauth
|
||||
omniauth-oauth2
|
||||
omniauth-oauth (1.1.0)
|
||||
oauth
|
||||
omniauth (~> 1.0)
|
||||
|
@ -463,6 +467,7 @@ DEPENDENCIES
|
|||
omniauth-gitlab
|
||||
omniauth-google-oauth2
|
||||
omniauth-linkedin-oauth2
|
||||
omniauth-microsoft-office365
|
||||
omniauth-oauth2
|
||||
omniauth-twitter
|
||||
pg
|
||||
|
@ -499,4 +504,4 @@ RUBY VERSION
|
|||
ruby 2.3.1p112
|
||||
|
||||
BUNDLED WITH
|
||||
1.13.7
|
||||
1.15.1
|
||||
|
|
|
@ -560,21 +560,24 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
|
||||
# inbound
|
||||
configureAttributesInbound = [
|
||||
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false },
|
||||
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off' },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
|
||||
]
|
||||
|
||||
showHideFolder = (params, attribute, attributes, classname, form, ui) ->
|
||||
return if !params
|
||||
if params.adapter is 'imap'
|
||||
ui.show('options::folder')
|
||||
ui.show('options::keep_on_server')
|
||||
return
|
||||
ui.hide('options::folder')
|
||||
ui.hide('options::keep_on_server')
|
||||
|
||||
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
||||
return if !params
|
||||
|
@ -606,9 +609,10 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
# fill user / password based on intro info
|
||||
channel_used = { options: {} }
|
||||
if @account['meta']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
channel_used['options']['password'] = @account['meta']['password']
|
||||
channel_used['options']['folder'] = @account['meta']['folder']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
channel_used['options']['password'] = @account['meta']['password']
|
||||
channel_used['options']['folder'] = @account['meta']['folder']
|
||||
channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server']
|
||||
|
||||
# show used backend
|
||||
@$('.base-outbound-settings').html('')
|
||||
|
@ -670,7 +674,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
for key, value of data.setting
|
||||
@account[key] = value
|
||||
|
||||
if data.content_messages && data.content_messages > 0
|
||||
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
||||
|
@ -724,7 +728,7 @@ class App.ChannelEmailAccountWizard extends App.WizardModal
|
|||
# remember account settings
|
||||
@account.inbound = params
|
||||
|
||||
if data.content_messages && data.content_messages > 0
|
||||
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
||||
|
|
|
@ -9,43 +9,7 @@ class Index extends App.ControllerSubContent
|
|||
@render()
|
||||
|
||||
render: =>
|
||||
auth_provider_all = {
|
||||
facebook: {
|
||||
url: '/auth/facebook'
|
||||
name: 'Facebook'
|
||||
config: 'auth_facebook'
|
||||
},
|
||||
twitter: {
|
||||
url: '/auth/twitter'
|
||||
name: 'Twitter'
|
||||
config: 'auth_twitter'
|
||||
},
|
||||
linkedin: {
|
||||
url: '/auth/linkedin'
|
||||
name: 'LinkedIn'
|
||||
config: 'auth_linkedin'
|
||||
},
|
||||
github: {
|
||||
url: '/auth/github'
|
||||
name: 'GitHub'
|
||||
config: 'auth_github'
|
||||
},
|
||||
gitlab: {
|
||||
url: '/auth/gitlab'
|
||||
name: 'GitLab'
|
||||
config: 'auth_gitlab'
|
||||
},
|
||||
google_oauth2: {
|
||||
url: '/auth/google_oauth2'
|
||||
name: 'Google'
|
||||
config: 'auth_google_oauth2'
|
||||
},
|
||||
oauth2: {
|
||||
url: '/auth/oauth2'
|
||||
name: 'OAuth2'
|
||||
config: 'auth_oauth2'
|
||||
},
|
||||
}
|
||||
auth_provider_all = App.Config.get('auth_provider_all')
|
||||
auth_providers = {}
|
||||
for key, provider of auth_provider_all
|
||||
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
|
||||
|
@ -90,3 +54,45 @@ class Index extends App.ControllerSubContent
|
|||
)
|
||||
|
||||
App.Config.set('LinkedAccounts', { prio: 4000, name: 'Linked Accounts', parent: '#profile', target: '#profile/linked', controller: Index, permission: ['user_preferences.linked_accounts'] }, 'NavBarProfile')
|
||||
App.Config.set('auth_provider_all', {
|
||||
facebook:
|
||||
url: '/auth/facebook'
|
||||
name: 'Facebook'
|
||||
config: 'auth_facebook'
|
||||
class: 'facebook'
|
||||
twitter:
|
||||
url: '/auth/twitter'
|
||||
name: 'Twitter'
|
||||
config: 'auth_twitter'
|
||||
class: 'twitter'
|
||||
linkedin:
|
||||
url: '/auth/linkedin'
|
||||
name: 'LinkedIn'
|
||||
config: 'auth_linkedin'
|
||||
class: 'linkedin'
|
||||
github:
|
||||
url: '/auth/github'
|
||||
name: 'GitHub'
|
||||
config: 'auth_github'
|
||||
class: 'github'
|
||||
gitlab:
|
||||
url: '/auth/gitlab'
|
||||
name: 'GitLab'
|
||||
config: 'auth_gitlab'
|
||||
class: 'gitlab'
|
||||
microsoft_office365:
|
||||
url: '/auth/microsoft_office365'
|
||||
name: 'Office 365'
|
||||
config: 'auth_microsoft_office365'
|
||||
class: 'office365'
|
||||
google_oauth2:
|
||||
url: '/auth/google_oauth2'
|
||||
name: 'Google'
|
||||
config: 'auth_google_oauth2'
|
||||
class: 'google'
|
||||
oauth2:
|
||||
url: '/auth/oauth2'
|
||||
name: 'OAuth2'
|
||||
config: 'auth_oauth2'
|
||||
class: 'oauth2'
|
||||
})
|
||||
|
|
|
@ -82,7 +82,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
view:
|
||||
shown: true
|
||||
invite_customer:
|
||||
show: false
|
||||
shown: false
|
||||
required: false
|
||||
'admin.user':
|
||||
create:
|
||||
|
@ -94,10 +94,10 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
view:
|
||||
shown: true
|
||||
invite_agent:
|
||||
show: false
|
||||
shown: false
|
||||
required: false
|
||||
invite_customer:
|
||||
show: false
|
||||
shown: false
|
||||
required: false
|
||||
Organization:
|
||||
'ticket.customer':
|
||||
|
|
|
@ -587,7 +587,7 @@ class ChatWindow extends App.Controller
|
|||
@sounds.message.play()
|
||||
@notifyDesktop(
|
||||
title: @name
|
||||
body: message
|
||||
body: App.Utils.html2text(message)
|
||||
url: '#customer_chat'
|
||||
callback: =>
|
||||
App.Event.trigger('chat_focus', { session_id: @session.session_id })
|
||||
|
|
|
@ -450,8 +450,8 @@ class EmailNotification extends App.WizardFullScreen
|
|||
if adapter is 'smtp'
|
||||
configureAttributesOutbound = [
|
||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password' },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off' },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true },
|
||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
||||
]
|
||||
@form = new App.ControllerForm(
|
||||
|
@ -671,20 +671,24 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
|
||||
# inbound
|
||||
configureAttributesInbound = [
|
||||
{ name: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||
{ name: 'options::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: 'adapter', display: 'Type', tag: 'select', multiple: false, null: false, options: @channelDriver.email.inbound },
|
||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: false, autocapitalize: false, autocomplete: 'off', single: true },
|
||||
{ name: 'options::ssl', display: 'SSL', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, default: true, translate: true, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false, default: '993', item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::folder', display: 'Folder', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, item_class: 'formGroup--halfSize' },
|
||||
{ name: 'options::keep_on_server', display: 'Keep messages on server', tag: 'boolean', null: true, options: { true: 'yes', false: 'no' }, translate: true, default: false, item_class: 'formGroup--halfSize' },
|
||||
]
|
||||
|
||||
showHideFolder = (params, attribute, attributes, classname, form, ui) ->
|
||||
return if !params
|
||||
if params.adapter is 'imap'
|
||||
ui.show('options::folder')
|
||||
ui.show('options::keep_on_server')
|
||||
return
|
||||
ui.hide('options::folder')
|
||||
ui.hide('options::keep_on_server')
|
||||
|
||||
handlePort = (params, attribute, attributes, classname, form, ui) ->
|
||||
return if !params
|
||||
|
@ -700,7 +704,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
return
|
||||
|
||||
new App.ControllerForm(
|
||||
el: @$('.base-inbound-settings'),
|
||||
el: @$('.base-inbound-settings')
|
||||
model:
|
||||
configure_attributes: configureAttributesInbound
|
||||
className: ''
|
||||
|
@ -716,8 +720,10 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
# fill user / password based on intro info
|
||||
channel_used = { options: {} }
|
||||
if @account['meta']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
channel_used['options']['password'] = @account['meta']['password']
|
||||
channel_used['options']['user'] = @account['meta']['email']
|
||||
channel_used['options']['password'] = @account['meta']['password']
|
||||
channel_used['options']['folder'] = @account['meta']['folder']
|
||||
channel_used['options']['keep_on_server'] = @account['meta']['keep_on_server']
|
||||
|
||||
# show used backend
|
||||
@$('.base-outbound-settings').html('')
|
||||
|
@ -725,8 +731,8 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
if adapter is 'smtp'
|
||||
configureAttributesOutbound = [
|
||||
{ name: 'options::host', display: 'Host', tag: 'input', type: 'text', limit: 120, null: false, autocapitalize: false, autofocus: true },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'new-password', single: true },
|
||||
{ name: 'options::user', display: 'User', tag: 'input', type: 'text', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', },
|
||||
{ name: 'options::password', display: 'Password', tag: 'input', type: 'password', limit: 120, null: true, autocapitalize: false, autocomplete: 'off', single: true },
|
||||
{ name: 'options::port', display: 'Port', tag: 'input', type: 'text', limit: 6, null: true, autocapitalize: false },
|
||||
]
|
||||
@form = new App.ControllerForm(
|
||||
|
@ -745,7 +751,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
@account.meta = params
|
||||
|
||||
@disable(e)
|
||||
@$('.js-probe .js-email').text( params.email )
|
||||
@$('.js-probe .js-email').text(params.email)
|
||||
@showSlide('js-probe')
|
||||
|
||||
@ajax(
|
||||
|
@ -760,7 +766,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
for key, value of data.setting
|
||||
@account[key] = value
|
||||
|
||||
if data.content_messages && data.content_messages > 0
|
||||
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||
message = App.i18n.translateContent('We have already found %s email(s) in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-intro')
|
||||
|
@ -809,7 +815,7 @@ class ChannelEmail extends App.WizardFullScreen
|
|||
# remember account settings
|
||||
@account.inbound = params
|
||||
|
||||
if data.content_messages && data.content_messages > 0
|
||||
if data.content_messages && data.content_messages > 0 && (!@account['inbound']['options'] || @account['inbound']['options']['keep_on_server'] isnt true)
|
||||
message = App.i18n.translateContent('We have already found %s emails in your mailbox. Zammad will move it all from your mailbox into Zammad.', data.content_messages)
|
||||
@$('.js-inbound-acknowledge .js-message').html(message)
|
||||
@$('.js-inbound-acknowledge .js-back').attr('data-slide', 'js-inbound')
|
||||
|
|
|
@ -38,50 +38,7 @@ class Index extends App.ControllerContent
|
|||
)
|
||||
|
||||
render: (data = {}) ->
|
||||
auth_provider_all = {
|
||||
facebook: {
|
||||
url: '/auth/facebook',
|
||||
name: 'Facebook',
|
||||
config: 'auth_facebook',
|
||||
class: 'facebook'
|
||||
},
|
||||
twitter: {
|
||||
url: '/auth/twitter'
|
||||
name: 'Twitter'
|
||||
config: 'auth_twitter'
|
||||
class: 'twitter'
|
||||
},
|
||||
linkedin: {
|
||||
url: '/auth/linkedin'
|
||||
name: 'LinkedIn'
|
||||
config: 'auth_linkedin'
|
||||
class: 'linkedin'
|
||||
},
|
||||
github: {
|
||||
url: '/auth/github'
|
||||
name: 'GitHub'
|
||||
config: 'auth_github'
|
||||
class: 'github'
|
||||
},
|
||||
gitlab: {
|
||||
url: '/auth/gitlab'
|
||||
name: 'GitLab'
|
||||
config: 'auth_gitlab'
|
||||
class: 'gitlab'
|
||||
},
|
||||
google_oauth2: {
|
||||
url: '/auth/google_oauth2'
|
||||
name: 'Google'
|
||||
config: 'auth_google_oauth2'
|
||||
class: 'google'
|
||||
},
|
||||
oauth2: {
|
||||
url: '/auth/oauth2'
|
||||
name: 'OAuth2'
|
||||
config: 'auth_oauth2'
|
||||
class: 'oauth2'
|
||||
},
|
||||
}
|
||||
auth_provider_all = App.Config.get('auth_provider_all')
|
||||
auth_providers = []
|
||||
for key, provider of auth_provider_all
|
||||
if @Config.get(provider.config) is true || @Config.get(provider.config) is 'true'
|
||||
|
|
|
@ -391,6 +391,7 @@ class App.TicketZoomArticleActions extends App.Controller
|
|||
body = @el.closest('.ticketZoom').find('.article-add [data-name="body"]').html() || ''
|
||||
|
||||
# check if quote need to be added
|
||||
signaturePosition = 'bottom'
|
||||
selected = App.ClipBoard.getSelected('html')
|
||||
if selected
|
||||
selected = App.Utils.htmlCleanup(selected).html()
|
||||
|
@ -399,6 +400,16 @@ class App.TicketZoomArticleActions extends App.Controller
|
|||
if selected
|
||||
selected = App.Utils.textCleanup(selected)
|
||||
selected = App.Utils.text2html(selected)
|
||||
|
||||
# full quote, if needed
|
||||
if !selected && article && App.Config.get('ui_ticket_zoom_article_email_full_quote')
|
||||
signaturePosition = 'top'
|
||||
if article.content_type.match('html')
|
||||
selected = App.Utils.textCleanup(article.body)
|
||||
if article.content_type.match('plain')
|
||||
selected = App.Utils.textCleanup(selected)
|
||||
selected = App.Utils.text2html(selected)
|
||||
|
||||
if selected
|
||||
selected = "<div><br><br/></div><div><blockquote type=\"cite\">#{selected}</blockquote></div><div><br></div>"
|
||||
|
||||
|
@ -409,7 +420,12 @@ class App.TicketZoomArticleActions extends App.Controller
|
|||
|
||||
type = App.TicketArticleType.findByAttribute(name:'email')
|
||||
|
||||
App.Event.trigger('ui::ticket::setArticleType', { ticket: @ticket, type: type, article: articleNew } )
|
||||
App.Event.trigger('ui::ticket::setArticleType', {
|
||||
ticket: @ticket
|
||||
type: type
|
||||
article: articleNew
|
||||
signaturePosition: signaturePosition
|
||||
})
|
||||
|
||||
telegramPersonalMessageReply: (e) =>
|
||||
e.preventDefault()
|
||||
|
|
|
@ -59,7 +59,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@$('[name="' + key + '"]').val(value).trigger('change')
|
||||
|
||||
# preselect article type
|
||||
@setArticleType(data.type.name)
|
||||
@setArticleType(data.type.name, data.signaturePosition)
|
||||
|
||||
# set focus at end of field
|
||||
if data.position is 'end'
|
||||
|
@ -483,7 +483,7 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
|
||||
@$('[name=internal]').val('')
|
||||
|
||||
setArticleType: (type) =>
|
||||
setArticleType: (type, signaturePosition = 'bottom') =>
|
||||
wasScrolledToBottom = @isScrolledToBottom()
|
||||
@type = type
|
||||
@$('[name=type]').val(type).trigger('change')
|
||||
|
@ -532,7 +532,10 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
body.append('<br><br>')
|
||||
signature = $("<div data-signature=\"true\" data-signature-id=\"#{signature.id}\">#{signatureFinished}</div>")
|
||||
App.Utils.htmlStrip(signature)
|
||||
body.append(signature)
|
||||
if signaturePosition is 'top'
|
||||
body.prepend(signature)
|
||||
else
|
||||
body.append(signature)
|
||||
@$('[data-name=body]').replaceWith(body)
|
||||
|
||||
# remove old signature
|
||||
|
@ -566,6 +569,20 @@ class App.TicketZoomArticleNew extends App.Controller
|
|||
@delay(@updateLetterCount, 600)
|
||||
@$('.js-textSizeLimit').removeClass('hide')
|
||||
|
||||
# convert remote src images to data uri
|
||||
@$('[data-name=body] img').each( (i,image) ->
|
||||
$image = $(image)
|
||||
src = $image.attr('src')
|
||||
if !_.isEmpty(src) && !src.match(/^data:image/i)
|
||||
canvas = document.createElement('canvas')
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(image, 0, 0)
|
||||
dataURL = canvas.toDataURL()
|
||||
$image.attr('src', dataURL)
|
||||
)
|
||||
|
||||
@scrollToBottom() if wasScrolledToBottom
|
||||
|
||||
isScrolledToBottom: ->
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
class SidebarOrganization extends App.Controller
|
||||
sidebarItem: =>
|
||||
return if !@permissionCheck('ticket.agent')
|
||||
return if !@ticket.organization_id
|
||||
{
|
||||
head: 'Organization'
|
||||
|
|
|
@ -5,10 +5,10 @@ class Widget
|
|||
banner = """
|
||||
|
|
||||
| Welcome Zammad Developer!
|
||||
| You can enable debugging by the following examples (value is a regex):
|
||||
| You can enable debugging with the following examples (value is a regex):
|
||||
|
|
||||
| App.Log.config('module', '(websocket|delay|interval)') // enable debugging for websocket, delay and interval class
|
||||
| App.Log.config('content', 'send') // enable debugging for messages which contains the string 'send'
|
||||
| App.Log.config('content', 'send') // enable debugging for messages which contain the string 'send'
|
||||
| App.Log.config('banner', false) // disable this banner
|
||||
|
|
||||
| App.Log.config() // current settings
|
||||
|
|
|
@ -38,11 +38,16 @@ class App.PrettyDate
|
|||
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
||||
month = months[created.getMonth()]
|
||||
|
||||
# for less than 7 days
|
||||
if diff < (60 * 60 * 24 * 7)
|
||||
# for less than 6 days
|
||||
# weekday HH::MM
|
||||
if diff < (60 * 60 * 24 * 6)
|
||||
string = "#{App.i18n.translateInline(weekday)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
|
||||
else if diff < (60 * 60 * 24 * 7) * 365
|
||||
# if it was this year
|
||||
# weekday DD. MM HH::MM
|
||||
else if created.getYear() is current.getYear()
|
||||
string = "#{App.i18n.translateInline(weekday)} #{created.getDate()}. #{App.i18n.translateInline(month)} #{created.getHours()}:#{@s(created.getMinutes(), 2)}"
|
||||
# if it was the year before
|
||||
# weekday YYYY-MM-DD HH::MM
|
||||
else
|
||||
string = "#{App.i18n.translateInline(weekday)} #{App.i18n.translateTimestamp(time)}"
|
||||
if escalation
|
||||
|
|
|
@ -295,7 +295,7 @@
|
|||
else {
|
||||
img = "<img style=\"width: 100%; max-width: " + width + "px;\" src=\"" + result + "\">"
|
||||
}
|
||||
document.execCommand('insertHTML', false, img)
|
||||
_this.paste(img)
|
||||
}
|
||||
|
||||
// resize if to big
|
||||
|
@ -367,13 +367,7 @@
|
|||
text = App.Utils.removeEmptyLines(text)
|
||||
_this.log('insert', text)
|
||||
|
||||
// as fallback, insert html via pasteHtmlAtCaret (for IE 11 and lower)
|
||||
if (docType == 'text3') {
|
||||
_this.pasteHtmlAtCaret(text)
|
||||
}
|
||||
else {
|
||||
document.execCommand('insertHTML', false, text)
|
||||
}
|
||||
_this.paste(text)
|
||||
return true
|
||||
})
|
||||
|
||||
|
@ -533,37 +527,6 @@
|
|||
return this.$element.html().trim()
|
||||
}
|
||||
|
||||
// taken from https://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
|
||||
Plugin.prototype.pasteHtmlAtCaret = function(html) {
|
||||
var sel, range;
|
||||
if (window.getSelection) {
|
||||
sel = window.getSelection()
|
||||
if (sel.getRangeAt && sel.rangeCount) {
|
||||
range = sel.getRangeAt(0)
|
||||
range.deleteContents()
|
||||
|
||||
var el = document.createElement('div')
|
||||
el.innerHTML = html;
|
||||
var frag = document.createDocumentFragment(), node, lastNode
|
||||
while ( (node = el.firstChild) ) {
|
||||
lastNode = frag.appendChild(node)
|
||||
}
|
||||
range.insertNode(frag)
|
||||
|
||||
if (lastNode) {
|
||||
range = range.cloneRange()
|
||||
range.setStartAfter(lastNode)
|
||||
range.collapse(true)
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (document.selection && document.selection.type != 'Control') {
|
||||
document.selection.createRange().pasteHTML(html)
|
||||
}
|
||||
}
|
||||
|
||||
// log method
|
||||
Plugin.prototype.log = function() {
|
||||
if (App && App.Log) {
|
||||
|
@ -574,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 () {
|
||||
if (!$.data(this, 'plugin_' + pluginName)) {
|
||||
$.data(this, 'plugin_' + pluginName,
|
||||
|
|
|
@ -250,9 +250,21 @@
|
|||
|
||||
// paste some content
|
||||
Plugin.prototype.paste = function(string) {
|
||||
if (document.selection) { // IE
|
||||
var isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
|
||||
|
||||
// IE <= 10
|
||||
if (document.selection && document.selection.createRange) {
|
||||
var range = document.selection.createRange()
|
||||
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 {
|
||||
document.execCommand('insertHTML', false, string)
|
||||
|
@ -295,14 +307,7 @@
|
|||
// for chrome, insert space again
|
||||
if (start) {
|
||||
if (spacerChar === ' ') {
|
||||
string = " "
|
||||
if (document.selection) { // IE
|
||||
var range = document.selection.createRange()
|
||||
range.pasteHTML(string)
|
||||
}
|
||||
else {
|
||||
document.execCommand('insertHTML', false, string)
|
||||
}
|
||||
this.paste(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ class App.Model extends Spine.Model
|
|||
return @title
|
||||
if @subject
|
||||
return @subject
|
||||
if @phone
|
||||
return @phone
|
||||
if @login
|
||||
return @login
|
||||
return '???'
|
||||
|
||||
displayNameLong: ->
|
||||
|
@ -57,6 +61,12 @@ class App.Model extends Spine.Model
|
|||
return @email
|
||||
if @title
|
||||
return @title
|
||||
if @subject
|
||||
return @subject
|
||||
if @phone
|
||||
return @phone
|
||||
if @login
|
||||
return @login
|
||||
return '???'
|
||||
|
||||
icon: (user) ->
|
||||
|
@ -165,6 +175,31 @@ class App.Model extends Spine.Model
|
|||
|
||||
###
|
||||
|
||||
set new attributes of model (remove already available attributes)
|
||||
|
||||
App.Model.attributesSet(attributes)
|
||||
|
||||
###
|
||||
|
||||
@attributesSet: (attributes) ->
|
||||
|
||||
configure_attributes = App[ @.className ].configure_attributes
|
||||
attributesNew = []
|
||||
for localAttribute in configure_attributes
|
||||
found = false
|
||||
for attribute in attributes
|
||||
if attribute.name is localAttribute.name
|
||||
found = true
|
||||
break
|
||||
if !found
|
||||
attributesNew.push localAttribute
|
||||
for attribute in attributes
|
||||
App[@.className].attributes.push attribute.name
|
||||
attributesNew.push attribute
|
||||
App[ @.className ].configure_attributes = attributesNew
|
||||
|
||||
###
|
||||
|
||||
attributes = App.Model.attributesGet(optionalScreen, optionalAttributesList)
|
||||
|
||||
returns
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
</form>
|
||||
|
||||
<form class="setup wizard hide js-inbound">
|
||||
<div class="wizard-slide">
|
||||
<div class="wizard-slide wizard-slide--large">
|
||||
<h2><%- @T('Email Inbound') %></h2>
|
||||
<div class="wizard-body vertical justified">
|
||||
<div class="alert alert--danger hide" role="alert"></div>
|
||||
|
|
|
@ -51,6 +51,10 @@
|
|||
<label for="id1">Name</label>
|
||||
<input id="id1" class="form-control" type="text" placeholder="Text Input">
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<label for="id1">Name (readonly)</label>
|
||||
<input id="id1" class="form-control" type="text" placeholder="Text Input" readonly value="Sandor Clegane">
|
||||
</div>
|
||||
<div class="input form-group">
|
||||
<label for="id2">Password</label>
|
||||
<input id="id2" class="form-control" type="password" value="Password Input">
|
||||
|
|
|
@ -116,8 +116,8 @@
|
|||
</div>
|
||||
|
||||
<div class="searchfield">
|
||||
<%- @Icon('magnifier') %>
|
||||
<input class="js-search form-control" name="search" placeholder="Search for users" type="search">
|
||||
<%- @Icon('magnifier') %>
|
||||
</div>
|
||||
|
||||
<div class="userSearch horizontal">
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<div class="formGroup-label">
|
||||
<label for="password"><%- @Ti('Password') %></label>
|
||||
</div>
|
||||
<input id="password" name="password" type="password" class="form-control"/>
|
||||
<input id="password" name="password" type="password" class="form-control" autocomplete="off"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
<div class="detail-search">
|
||||
<div class="detail-search-header">
|
||||
<div class="searchfield">
|
||||
<%- @Icon('magnifier') %>
|
||||
<input class="js-search form-control<%= if !@query then ' is-empty' %>" name="query" placeholder="<%- @Ti('Find what you search. E. g. "search phrase"') %>" value="<%= @query %>" type="search" autocomplete="off">
|
||||
<%- @Icon('magnifier') %>
|
||||
<div class="empty-search js-emptySearch">
|
||||
<%- @Icon('diagonal-cross') %>
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<div class="ticketZoom">
|
||||
<div class="ticketZoom-controls">
|
||||
<div class="js-settingContainer"></div>
|
||||
<div class="spacer"></div>
|
||||
<div class="js-highlighterContainer highlighter"></div>
|
||||
<div class="js-overviewNavigatorContainer overview-navigator"></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') %>
|
||||
</div>
|
|
@ -12,8 +12,8 @@
|
|||
</div>
|
||||
<div class="page-content">
|
||||
<div class="searchfield">
|
||||
<%- @Icon('magnifier') %>
|
||||
<input class="js-search form-control" name="search" placeholder="<%- @Ti('Search for users') %>" type="search">
|
||||
<%- @Icon('magnifier') %>
|
||||
</div>
|
||||
|
||||
<div class="userSearch horizontal">
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
.icon-mute { width: 16px; height: 16px; }
|
||||
.icon-note { width: 16px; height: 16px; }
|
||||
.icon-oauth2-button { width: 29px; height: 24px; }
|
||||
.icon-office365-button { width: 29px; height: 24px; }
|
||||
.icon-one-ticket { width: 48px; height: 10px; }
|
||||
.icon-organization { width: 16px; height: 16px; }
|
||||
.icon-outbound-calls { width: 17px; height: 17px; }
|
||||
|
|
|
@ -398,7 +398,8 @@ pre code.hljs {
|
|||
}
|
||||
|
||||
&.is-disabled,
|
||||
&[disabled] {
|
||||
&[disabled],
|
||||
&:disabled {
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
opacity: .33;
|
||||
|
@ -415,7 +416,7 @@ pre code.hljs {
|
|||
font-size: 12px;
|
||||
letter-spacing: 0.05em;
|
||||
height: 31px;
|
||||
padding: 2px 11px 0 !important;
|
||||
padding: 0 11px !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
|
@ -1623,6 +1624,24 @@ textarea,
|
|||
border-color: hsl(200,71%,59%);
|
||||
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] {
|
||||
|
@ -1700,13 +1719,6 @@ select.form-control:not([multiple]) {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.form-control[disabled], .form-control.is-disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: #fff;
|
||||
color: #d5d5d5;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-control.form-control--borderless {
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
@ -1860,6 +1872,7 @@ input.has-error {
|
|||
appearance: textfield;
|
||||
border-radius: 19px;
|
||||
padding: 0 17px 0 42px;
|
||||
will-change: transform;
|
||||
|
||||
&.is-empty + .empty-search {
|
||||
visibility: hidden;
|
||||
|
@ -2550,6 +2563,10 @@ ol.tabs li {
|
|||
background: hsl(0,0%,15%);
|
||||
}
|
||||
|
||||
&.auth-provider--office365 {
|
||||
background: hsl(15,100%,47%);
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -2558,7 +2575,6 @@ ol.tabs li {
|
|||
width: 29px;
|
||||
height: 24px;
|
||||
margin-right: 10px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3956,7 +3972,6 @@ footer {
|
|||
max-width: 400px;
|
||||
min-width: 350px;
|
||||
flex-direction: column;
|
||||
@extend .zIndex-2;
|
||||
|
||||
&.is-visible {
|
||||
display: flex;
|
||||
|
@ -5483,8 +5498,13 @@ footer {
|
|||
.newTicket .sidebar {
|
||||
width: 290px;
|
||||
}
|
||||
.newTicket .form-control:not(:focus):not(.focus) {
|
||||
.newTicket .form-control {
|
||||
border-color: hsl(0,0%,90%);
|
||||
|
||||
&:focus,
|
||||
&.focus {
|
||||
border-color: hsl(200,71%,59%);
|
||||
}
|
||||
}
|
||||
.newTicket .article-form-top {
|
||||
margin-top: 15px;
|
||||
|
@ -5964,6 +5984,8 @@ footer {
|
|||
}
|
||||
|
||||
.dropdown-menu {
|
||||
@extend .zIndex-5; // has to be behind modal windows and beneath notifications (popover)
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 100%;
|
||||
|
@ -6303,15 +6325,18 @@ footer {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.checkbox.form-group .controls label {
|
||||
padding: 2px 0;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
margin-bottom: 0;
|
||||
color: inherit;
|
||||
text-transform: inherit;
|
||||
letter-spacing: 0;
|
||||
@extend .u-clickable;
|
||||
.checkbox,
|
||||
.radio {
|
||||
&.form-group .controls label {
|
||||
padding: 2px 0;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
margin-bottom: 0;
|
||||
color: inherit;
|
||||
text-transform: inherit;
|
||||
letter-spacing: 0;
|
||||
@extend .u-clickable;
|
||||
}
|
||||
}
|
||||
|
||||
.userSearch-label {
|
||||
|
@ -6440,6 +6465,9 @@ footer {
|
|||
@extend .u-textTruncate;
|
||||
}
|
||||
|
||||
.wizard {
|
||||
margin: auto; // makes sure that the wizard is scrollable
|
||||
}
|
||||
|
||||
.wizard-logo {
|
||||
fill: white;
|
||||
|
@ -6454,6 +6482,10 @@ footer {
|
|||
width: 400px;
|
||||
padding-bottom: 18px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&.wizard-slide--large {
|
||||
width: 460px;
|
||||
}
|
||||
}
|
||||
|
||||
.wizard h2 {
|
||||
|
@ -7178,6 +7210,12 @@ output {
|
|||
.zammad-switch input {
|
||||
display: none;
|
||||
|
||||
&[disabled] + label {
|
||||
cursor: not-allowed;
|
||||
background: hsl(210,17%,93%);
|
||||
border-color: hsl(210,10%,85%);
|
||||
}
|
||||
|
||||
&:focus + label {
|
||||
transition: none;
|
||||
background: hsl(200,71%,59%);
|
||||
|
|
|
@ -126,6 +126,16 @@ class UsersController < ApplicationController
|
|||
if admin_account_exists && !params[:signup]
|
||||
raise Exceptions::UnprocessableEntity, 'Only signup with not authenticate user possible!'
|
||||
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.associations_from_param(params)
|
||||
user.updated_by_id = 1
|
||||
|
@ -165,11 +175,6 @@ class UsersController < ApplicationController
|
|||
user.associations_from_param(params)
|
||||
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!
|
||||
|
||||
# if first user was added, set system init done
|
||||
|
@ -177,7 +182,7 @@ class UsersController < ApplicationController
|
|||
Setting.set('system_init_done', true)
|
||||
|
||||
# fetch org logo
|
||||
if !user.email.empty?
|
||||
if user.email.present?
|
||||
Service::Image.organization_suggest(user.email)
|
||||
end
|
||||
|
||||
|
@ -252,17 +257,17 @@ class UsersController < ApplicationController
|
|||
|
||||
# only allow Admin's
|
||||
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
|
||||
|
||||
# only allow Admin's
|
||||
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
|
||||
|
||||
# only allow Admin's and Agent's
|
||||
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
|
||||
|
||||
if params[:expand]
|
||||
|
@ -363,7 +368,7 @@ class UsersController < ApplicationController
|
|||
limit: params[:limit],
|
||||
current_user: current_user,
|
||||
}
|
||||
if params[:role_ids] && !params[:role_ids].empty?
|
||||
if params[:role_ids].present?
|
||||
query_params[:role_ids] = params[:role_ids]
|
||||
end
|
||||
|
||||
|
@ -449,10 +454,10 @@ class UsersController < ApplicationController
|
|||
end
|
||||
|
||||
# do query
|
||||
user_all = if params[:role_ids] && !params[:role_ids].empty?
|
||||
User.joins(:roles).where( 'roles.id' => params[:role_ids] ).where('users.id != 1').order('users.created_at DESC').limit( params[:limit] || 20 )
|
||||
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)
|
||||
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
|
||||
|
||||
# build result list
|
||||
|
|
|
@ -5,7 +5,7 @@ class ApplicationModel < ActiveRecord::Base
|
|||
include ApplicationModel::HasCache
|
||||
include ApplicationModel::CanLookup
|
||||
include ApplicationModel::CanLookupSearchIndexAttributes
|
||||
include ApplicationModel::ChecksAttributeLength
|
||||
include ApplicationModel::ChecksAttributeValuesAndLength
|
||||
include ApplicationModel::CanCleanupParam
|
||||
include ApplicationModel::HasRecentViews
|
||||
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',
|
||||
user: 'xxx@znuny.onmicrosoft.com',
|
||||
password: 'xxx',
|
||||
keep_on_server: true,
|
||||
}
|
||||
channel = Channel.last
|
||||
instance = Channel::Driver::Imap.new
|
||||
|
@ -60,13 +61,18 @@ example
|
|||
=end
|
||||
|
||||
def fetch (options, channel, check_type = '', verify_string = '')
|
||||
ssl = true
|
||||
port = 993
|
||||
ssl = true
|
||||
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
|
||||
ssl = false
|
||||
port = 143
|
||||
end
|
||||
if options.key?(:port) && !options[:port].empty?
|
||||
if options.key?(:port) && options[:port].present?
|
||||
port = options[:port]
|
||||
|
||||
# disable ssl for non ssl ports
|
||||
|
@ -74,8 +80,11 @@ example
|
|||
ssl = false
|
||||
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
|
||||
timeout = 45
|
||||
|
@ -90,17 +99,17 @@ example
|
|||
@imap.login(options[:user], options[:password])
|
||||
|
||||
# select folder
|
||||
if !options[:folder] || options[:folder].empty?
|
||||
@imap.select('INBOX')
|
||||
else
|
||||
@imap.select(options[:folder])
|
||||
end
|
||||
@imap.select(folder)
|
||||
|
||||
# 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
|
||||
message_ids = @imap.sort(['DATE'], ['ALL'], 'US-ASCII')
|
||||
message_ids = @imap.sort(['DATE'], filter, 'US-ASCII')
|
||||
rescue
|
||||
message_ids = @imap.search(['ALL'])
|
||||
message_ids = @imap.search(filter)
|
||||
end
|
||||
|
||||
# check mode only
|
||||
|
@ -168,9 +177,8 @@ example
|
|||
message_ids.each do |message_id|
|
||||
count += 1
|
||||
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
|
||||
info = too_big?(message_meta, count, count_all)
|
||||
|
@ -182,14 +190,23 @@ example
|
|||
# ignore deleted messages
|
||||
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
|
||||
msg = @imap.fetch(message_id, 'RFC822')[0].attr['RFC822']
|
||||
next if !msg
|
||||
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
|
||||
end
|
||||
@imap.expunge()
|
||||
if !keep_on_server
|
||||
@imap.expunge()
|
||||
end
|
||||
disconnect
|
||||
if count.zero?
|
||||
Rails.logger.info ' - no message'
|
||||
|
@ -209,6 +226,20 @@ example
|
|||
|
||||
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)
|
||||
return false if !message_meta.attr['FLAGS'].include?(:Deleted)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
=begin
|
||||
|
|
|
@ -358,5 +358,23 @@ returns
|
|||
)
|
||||
}
|
||||
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
|
||||
|
|
|
@ -28,24 +28,37 @@ class Overview < ApplicationModel
|
|||
end
|
||||
|
||||
def fill_link_on_create
|
||||
return true if !link.empty?
|
||||
return true if link.present?
|
||||
self.link = link_name(name)
|
||||
true
|
||||
end
|
||||
|
||||
def fill_link_on_update
|
||||
return true if link.empty?
|
||||
return true if !changes['name']
|
||||
return true if changes['link']
|
||||
self.link = link_name(name)
|
||||
true
|
||||
end
|
||||
|
||||
def link_name(name)
|
||||
link = name.downcase
|
||||
link.gsub!(/\s/, '_')
|
||||
link.gsub!(/[^0-9a-z]/i, '_')
|
||||
link.gsub!(/_+/, '_')
|
||||
link
|
||||
local_link = name.downcase
|
||||
local_link = local_link.parameterize('_')
|
||||
local_link.gsub!(/\s/, '_')
|
||||
local_link.gsub!(/_+/, '_')
|
||||
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
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
|
||||
class Signature < ApplicationModel
|
||||
include ChecksLatestChangeObserved
|
||||
include ChecksHtmlSanitized
|
||||
|
||||
has_many :groups, after_add: :cache_update, after_remove: :cache_update
|
||||
validates :name, presence: true
|
||||
|
||||
sanitized_html :body
|
||||
|
||||
end
|
||||
|
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
class TextModule < ApplicationModel
|
||||
include ChecksClientNotification
|
||||
include ChecksHtmlSanitized
|
||||
|
||||
validates :name, presence: true
|
||||
validates :content, presence: true
|
||||
|
||||
sanitized_html :content
|
||||
|
||||
=begin
|
||||
|
||||
load text modules from online
|
||||
|
|
|
@ -38,7 +38,7 @@ class User < ApplicationModel
|
|||
load 'user/search_index.rb'
|
||||
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_update :check_preferences_default, :validate_roles, :reset_login_failed
|
||||
after_create :avatar_for_email_check
|
||||
|
@ -845,7 +845,7 @@ returns
|
|||
|
||||
def check_email
|
||||
return true if Setting.get('import_mode')
|
||||
return true if email.empty?
|
||||
return true if email.blank?
|
||||
self.email = email.downcase.strip
|
||||
return true if id == 1
|
||||
raise Exceptions::UnprocessableEntity, 'Invalid email' if email !~ /@/
|
||||
|
@ -867,9 +867,9 @@ returns
|
|||
end
|
||||
end
|
||||
|
||||
# if no email, complain about missing login
|
||||
if id != 1 && login.blank?
|
||||
raise Exceptions::UnprocessableEntity, 'Attribute \'login\' required!'
|
||||
# generate auto login
|
||||
if login.blank?
|
||||
self.login = "auto-#{Time.zone.now.to_i}-#{rand(999_999)}"
|
||||
end
|
||||
|
||||
# check if login already exists
|
||||
|
@ -878,7 +878,7 @@ returns
|
|||
while check
|
||||
exists = User.find_by(login: login)
|
||||
if exists && exists.id != id
|
||||
self.login = login + rand(999).to_s
|
||||
self.login = "#{login}#{rand(999)}"
|
||||
else
|
||||
check = false
|
||||
end
|
||||
|
@ -886,6 +886,27 @@ returns
|
|||
true
|
||||
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
|
||||
return true if !role_ids
|
||||
role_ids.each { |role_id|
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# mysql
|
||||
if ActiveRecord::Base.connection_config[:adapter] == 'mysql2'
|
||||
Rails.application.config.db_4bytes_utf8 = false
|
||||
Rails.application.config.db_null_byte = true
|
||||
|
||||
# mysql version check
|
||||
# mysql example: "5.7.3"
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
if ActiveRecord::Base.connection_config[:adapter] == 'postgresql'
|
||||
Rails.application.config.db_case_sensitive = true
|
||||
Rails.application.config.db_like = 'ILIKE'
|
||||
Rails.application.config.db_null_byte = false
|
||||
|
||||
# postgresql version check
|
||||
# example output: "9.5.0"
|
||||
|
|
|
@ -32,9 +32,11 @@ Rails.application.config.middleware.use OmniAuth::Builder do
|
|||
authorize_url: '/oauth/authorize',
|
||||
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
|
||||
provider :oauth2_database, 'not_change_will_be_set_by_database', 'not_change_will_be_set_by_database', {
|
||||
client_options: {
|
||||
|
|
Binary file not shown.
|
@ -33,6 +33,7 @@ server {
|
|||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header CLIENT_IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_pass http://zammad-websocket;
|
||||
}
|
||||
|
@ -41,6 +42,7 @@ server {
|
|||
proxy_set_header Host $http_host;
|
||||
proxy_set_header CLIENT_IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 180;
|
||||
proxy_pass http://zammad;
|
||||
|
||||
|
|
|
@ -124,6 +124,7 @@ server {
|
|||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header CLIENT_IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_pass http://zammad-websocket;
|
||||
}
|
||||
|
@ -132,6 +133,7 @@ server {
|
|||
proxy_set_header Host $http_host;
|
||||
proxy_set_header CLIENT_IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 180;
|
||||
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
|
||||
|
||||
# return if it's a new setup
|
||||
|
@ -67,6 +67,33 @@ class TicketZoomSetting < ActiveRecord::Migration
|
|||
},
|
||||
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(
|
||||
title: 'Twitter - tweet 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: {
|
||||
type: 'email',
|
||||
maxlength: 150,
|
||||
null: false,
|
||||
null: true,
|
||||
item_class: 'formGroup--halfSize',
|
||||
},
|
||||
editable: false,
|
||||
|
@ -627,7 +627,7 @@ ObjectManager::Attribute.add(
|
|||
},
|
||||
edit: {
|
||||
'-all-' => {
|
||||
null: false,
|
||||
null: true,
|
||||
},
|
||||
},
|
||||
view: {
|
||||
|
|
|
@ -156,6 +156,15 @@ Scheduler.create_if_not_exists(
|
|||
updated_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(
|
||||
name: 'Import Jobs',
|
||||
method: 'ImportJob.start_registered',
|
||||
|
|
|
@ -600,6 +600,33 @@ Setting.create_if_not_exists(
|
|||
},
|
||||
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(
|
||||
title: 'Twitter - tweet initials',
|
||||
name: 'ui_ticket_zoom_article_twitter_initials',
|
||||
|
@ -678,6 +705,31 @@ Setting.create_if_not_exists(
|
|||
},
|
||||
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(
|
||||
title: 'Authentication via %s',
|
||||
name: 'auth_ldap',
|
||||
|
@ -1057,6 +1109,63 @@ Setting.create_if_not_exists(
|
|||
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(
|
||||
title: 'Authentication via %s',
|
||||
name: 'auth_oauth2',
|
||||
|
@ -1116,7 +1225,7 @@ Setting.create_if_not_exists(
|
|||
null: true,
|
||||
name: 'site',
|
||||
tag: 'input',
|
||||
placeholder: 'https://gitlab.YOURDOMAIN.com',
|
||||
placeholder: 'https://oauth.YOURDOMAIN.com',
|
||||
},
|
||||
{
|
||||
display: 'authorize_url',
|
||||
|
|
|
@ -81,7 +81,7 @@ class String
|
|||
def html2text(string_only = false, strict = false)
|
||||
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
|
||||
# note: string.encode!('UTF-8', 'UTF-8', :invalid => :replace, :replace => '?') was not detecting invalid chars
|
||||
if !string.valid_encoding?
|
||||
|
|
|
@ -66,6 +66,7 @@ class Stats::TicketReopen
|
|||
def self.log(object, o_id, changes, updated_by_id)
|
||||
return if object != 'Ticket'
|
||||
ticket = Ticket.lookup(id: o_id)
|
||||
return if !ticket
|
||||
|
||||
# check if close_at is already set / if not, ticket is not reopend
|
||||
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 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!'
|
||||
'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':
|
||||
'<strong>Chat</strong> with us!': '<strong>Chattez</strong> avec nous!'
|
||||
'Scroll down to see new messages': 'Faites défiler pour lire les nouveaux messages'
|
||||
|
@ -251,6 +268,11 @@ do($ = window.jQuery, window) ->
|
|||
sessionId: undefined
|
||||
scrolledToBottom: true
|
||||
scrollSnapTolerance: 10
|
||||
richTextFormatKey:
|
||||
66: true # b
|
||||
73: true # i
|
||||
85: true # u
|
||||
83: true # s
|
||||
|
||||
T: (string, items...) =>
|
||||
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-body').on 'scroll', @detectScrolledtoBottom
|
||||
@el.find('.zammad-scroll-hint').click @onScrollHintClick
|
||||
@input.on
|
||||
@input.on(
|
||||
keydown: @checkForEnter
|
||||
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', =>
|
||||
@onLeaveTemporary()
|
||||
)
|
||||
|
@ -471,7 +695,7 @@ do($ = window.jQuery, window) ->
|
|||
from: if message.created_by_id then 'agent' else 'customer'
|
||||
|
||||
if unfinishedMessage
|
||||
@input.val unfinishedMessage
|
||||
@input.html(unfinishedMessage)
|
||||
|
||||
# show wait list
|
||||
if data.position
|
||||
|
@ -489,7 +713,7 @@ do($ = window.jQuery, window) ->
|
|||
@el.find('.zammad-chat-message--unread')
|
||||
.removeClass 'zammad-chat-message--unread'
|
||||
|
||||
sessionStorage.setItem 'unfinished_message', @input.val()
|
||||
sessionStorage.setItem 'unfinished_message', @input.html()
|
||||
|
||||
@onTyping()
|
||||
|
||||
|
@ -520,7 +744,7 @@ do($ = window.jQuery, window) ->
|
|||
@sendMessage()
|
||||
|
||||
sendMessage: ->
|
||||
message = @input.val()
|
||||
message = @input.html()
|
||||
return if !message
|
||||
|
||||
@inactiveTimeout.start()
|
||||
|
@ -543,7 +767,7 @@ do($ = window.jQuery, window) ->
|
|||
@lastAddedType = 'message--customer'
|
||||
@el.find('.zammad-chat-body').append messageElement
|
||||
|
||||
@input.val('')
|
||||
@input.html('')
|
||||
@scrollToBottom()
|
||||
|
||||
# send message event
|
||||
|
@ -585,11 +809,6 @@ do($ = window.jQuery, window) ->
|
|||
|
||||
@el.addClass('zammad-chat-is-open')
|
||||
|
||||
if !@inputInitialized
|
||||
@inputInitialized = true
|
||||
@input.autoGrow
|
||||
extraLine: false
|
||||
|
||||
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
|
||||
|
||||
@el.css 'bottom', -remainerHeight
|
||||
|
@ -1032,4 +1251,204 @@ do($ = window.jQuery, window) ->
|
|||
else if direction is 'horizontal'
|
||||
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
|
||||
|
|
|
@ -320,9 +320,9 @@
|
|||
.zammad-chat-controls {
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
-webkit-align-items: flex-start;
|
||||
-ms-flex-align: start;
|
||||
align-items: flex-start;
|
||||
-webkit-align-items: flex-end;
|
||||
-ms-flex-align: end;
|
||||
align-items: flex-end;
|
||||
border-top: 1px solid #ededed;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
@ -340,25 +340,23 @@
|
|||
margin: 0;
|
||||
padding: 1em 2em;
|
||||
float: left;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: 6em;
|
||||
min-height: 1.4em !important;
|
||||
min-height: 1.4em;
|
||||
font-family: inherit;
|
||||
line-height: 1.4em;
|
||||
font-size: inherit;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: none !important;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none !important;
|
||||
box-shadow: none;
|
||||
box-sizing: content-box;
|
||||
outline: none;
|
||||
resize: none;
|
||||
-webkit-flex: 1;
|
||||
-ms-flex: 1;
|
||||
flex: 1; }
|
||||
flex: 1;
|
||||
overflow: auto; }
|
||||
|
||||
.zammad-chat-input::-webkit-input-placeholder {
|
||||
color: #d9d9d9; }
|
||||
|
@ -373,7 +371,7 @@
|
|||
background: #379ad7;
|
||||
color: white;
|
||||
padding: 0.5em 1.2em;
|
||||
margin: 0.5em 1em 0.5em;
|
||||
margin: 0.63em 1em;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
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); }; },
|
||||
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; },
|
||||
|
@ -60,7 +121,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
Log.prototype.log = function(level, items) {
|
||||
var i, item, len, logString;
|
||||
var item, j, len, logString;
|
||||
items.unshift('||');
|
||||
items.unshift(level);
|
||||
items.unshift(this.options.logPrefix);
|
||||
|
@ -69,8 +130,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
return;
|
||||
}
|
||||
logString = '';
|
||||
for (i = 0, len = items.length; i < len; i++) {
|
||||
item = items[i];
|
||||
for (j = 0, len = items.length; j < len; j++) {
|
||||
item = items[j];
|
||||
logString += ' ';
|
||||
if (typeof item === 'object') {
|
||||
logString += JSON.stringify(item);
|
||||
|
@ -173,11 +234,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
})(this);
|
||||
this.ws.onmessage = (function(_this) {
|
||||
return function(e) {
|
||||
var i, len, pipe, pipes;
|
||||
var j, len, pipe, pipes;
|
||||
pipes = JSON.parse(e.data);
|
||||
_this.log.debug('onMessage', e.data);
|
||||
for (i = 0, len = pipes.length; i < len; i++) {
|
||||
pipe = pipes[i];
|
||||
for (j = 0, len = pipes.length; j < len; j++) {
|
||||
pipe = pipes[j];
|
||||
if (pipe.event === 'pong') {
|
||||
_this.ping();
|
||||
}
|
||||
|
@ -386,8 +447,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
|
||||
ZammadChat.prototype.scrollSnapTolerance = 10;
|
||||
|
||||
ZammadChat.prototype.richTextFormatKey = {
|
||||
66: true,
|
||||
73: true,
|
||||
85: true,
|
||||
83: true
|
||||
};
|
||||
|
||||
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) : [];
|
||||
if (this.options.lang && this.options.lang !== 'en') {
|
||||
if (!this.translations[this.options.lang]) {
|
||||
|
@ -401,8 +469,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
}
|
||||
}
|
||||
if (items) {
|
||||
for (i = 0, len = items.length; i < len; i++) {
|
||||
item = items[i];
|
||||
for (j = 0, len = items.length; j < len; j++) {
|
||||
item = items[j];
|
||||
string = string.replace(/%s/, item);
|
||||
}
|
||||
}
|
||||
|
@ -425,6 +493,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
function ZammadChat(options) {
|
||||
this.removeAttributes = bind(this.removeAttributes, this);
|
||||
this.startTimeoutObservers = bind(this.startTimeoutObservers, this);
|
||||
this.onCssLoaded = bind(this.onCssLoaded, 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,
|
||||
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) {
|
||||
return function() {
|
||||
return _this.onLeaveTemporary();
|
||||
|
@ -595,9 +861,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
ZammadChat.prototype.onWebSocketMessage = function(pipes) {
|
||||
var i, len, pipe;
|
||||
for (i = 0, len = pipes.length; i < len; i++) {
|
||||
pipe = pipes[i];
|
||||
var j, len, pipe;
|
||||
for (j = 0, len = pipes.length; j < len; j++) {
|
||||
pipe = pipes[j];
|
||||
this.log.debug('ws:onmessage', pipe);
|
||||
switch (pipe.event) {
|
||||
case 'chat_error':
|
||||
|
@ -683,15 +949,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
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.inactiveTimeout.start();
|
||||
unfinishedMessage = sessionStorage.getItem('unfinished_message');
|
||||
if (data.agent) {
|
||||
this.onConnectionEstablished(data);
|
||||
ref = data.session;
|
||||
for (i = 0, len = ref.length; i < len; i++) {
|
||||
message = ref[i];
|
||||
for (j = 0, len = ref.length; j < len; j++) {
|
||||
message = ref[j];
|
||||
this.renderMessage({
|
||||
message: message.content,
|
||||
id: message.id,
|
||||
|
@ -699,7 +965,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
});
|
||||
}
|
||||
if (unfinishedMessage) {
|
||||
this.input.val(unfinishedMessage);
|
||||
this.input.html(unfinishedMessage);
|
||||
}
|
||||
}
|
||||
if (data.position) {
|
||||
|
@ -715,7 +981,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
|
||||
ZammadChat.prototype.onInput = function() {
|
||||
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();
|
||||
};
|
||||
|
||||
|
@ -749,7 +1015,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
|
||||
ZammadChat.prototype.sendMessage = function() {
|
||||
var message, messageElement;
|
||||
message = this.input.val();
|
||||
message = this.input.html();
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
@ -769,7 +1035,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.lastAddedType = 'message--customer';
|
||||
this.el.find('.zammad-chat-body').append(messageElement);
|
||||
}
|
||||
this.input.val('');
|
||||
this.input.html('');
|
||||
this.scrollToBottom();
|
||||
return this.send('chat_session_message', {
|
||||
content: message,
|
||||
|
@ -810,12 +1076,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.showLoader();
|
||||
}
|
||||
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();
|
||||
this.el.css('bottom', -remainerHeight);
|
||||
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;
|
||||
|
||||
})(Base);
|
||||
return window.ZammadChat = ZammadChat;
|
||||
})(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) {
|
||||
window.zammadChatTemplates = {};
|
||||
}
|
||||
|
@ -1555,11 +1873,11 @@ window.zammadChatTemplates["chat"] = function (__obj) {
|
|||
|
||||
__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('"></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) {
|
||||
__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 {
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
align-items: flex-start;
|
||||
align-items: flex-end;
|
||||
border-top: 1px solid hsl(0,0%,93%);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
@ -349,21 +349,19 @@
|
|||
margin: 0;
|
||||
padding: 1em 2em;
|
||||
float: left;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-height: 6em;
|
||||
min-height: 1.4em !important;
|
||||
min-height: 1.4em;
|
||||
font-family: inherit;
|
||||
line-height: 1.4em;
|
||||
font-size: inherit;
|
||||
appearance: none;
|
||||
border: none !important;
|
||||
border: none;
|
||||
background: none;
|
||||
box-shadow: none !important;
|
||||
box-shadow: none ;
|
||||
box-sizing: content-box;
|
||||
outline: none;
|
||||
resize: none;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.zammad-chat-input::-webkit-input-placeholder {
|
||||
|
@ -378,7 +376,7 @@
|
|||
background: hsl(203,67%,53%);
|
||||
color: white;
|
||||
padding: 0.5em 1.2em;
|
||||
margin: 0.5em 1em 0.5em;
|
||||
margin: 0.63em 1em;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 1.5em;
|
||||
|
|
|
@ -29,9 +29,7 @@ gulp.task('js', function(){
|
|||
.pipe(plumber())
|
||||
.pipe(coffee({bare: true}).on('error', gutil.log));
|
||||
|
||||
var autoGrow = gulp.src('jquery.autoGrow.js');
|
||||
|
||||
return merge(templates, js, autoGrow)
|
||||
return merge(templates, js)
|
||||
.pipe(concat('chat.js'))
|
||||
.pipe(gulp.dest('./'))
|
||||
.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 class="zammad-chat-body"></div>
|
||||
<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>
|
||||
</form>
|
||||
</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)
|
||||
result = JSON.parse(@response.body)
|
||||
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)
|
||||
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)
|
||||
result = JSON.parse(@response.body)
|
||||
assert(result)
|
||||
assert_equal('User already exists!', result['error'])
|
||||
assert_equal('Email address is already used for other user.', result['error'])
|
||||
|
||||
# missing required attributes
|
||||
params = { note: 'some note' }
|
||||
|
@ -330,15 +346,9 @@ class UserOrganizationControllerTest < ActionDispatch::IntegrationTest
|
|||
assert_response(422)
|
||||
result = JSON.parse(@response.body)
|
||||
assert(result)
|
||||
assert_equal('Attribute \'login\' 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'])
|
||||
assert_equal('Minimum one identifier (login, firstname, lastname, phone or email) for user is required.', result['error'])
|
||||
|
||||
# invalid email
|
||||
params = { firstname: 'newfirstname123', email: 'some_what', note: 'some note' }
|
||||
post '/api/v1/users', params.to_json, @headers.merge('Authorization' => credentials)
|
||||
assert_response(422)
|
||||
|
@ -346,6 +356,20 @@ class UserOrganizationControllerTest < ActionDispatch::IntegrationTest
|
|||
assert(result)
|
||||
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
|
||||
|
||||
test 'user index and create with agent' do
|
||||
|
|
|
@ -4,16 +4,16 @@ require 'test_helper'
|
|||
class EmailDeliverTest < ActiveSupport::TestCase
|
||||
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'"
|
||||
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'"
|
||||
end
|
||||
server_login = ENV['MAIL_SERVER_ACCOUNT'].split(':')[0]
|
||||
server_password = ENV['MAIL_SERVER_ACCOUNT'].split(':')[1]
|
||||
|
||||
email_address = EmailAddress.create(
|
||||
email_address = EmailAddress.create!(
|
||||
realname: 'me Helpdesk',
|
||||
email: "me#{rand(999_999_999)}@example.com",
|
||||
updated_by_id: 1,
|
||||
|
@ -27,7 +27,7 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
|||
created_by_id: 1,
|
||||
)
|
||||
|
||||
channel = Channel.create(
|
||||
channel = Channel.create!(
|
||||
area: 'Email::Account',
|
||||
group_id: group.id,
|
||||
options: {
|
||||
|
@ -50,9 +50,9 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
|||
)
|
||||
|
||||
email_address.channel_id = channel.id
|
||||
email_address.save
|
||||
email_address.save!
|
||||
|
||||
ticket1 = Ticket.create(
|
||||
ticket1 = Ticket.create!(
|
||||
title: 'some delivery test',
|
||||
group: group,
|
||||
customer_id: 2,
|
||||
|
@ -63,7 +63,7 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
|||
)
|
||||
assert(ticket1, 'ticket created')
|
||||
|
||||
article1 = Ticket::Article.create(
|
||||
article1 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
to: 'some_recipient@example_not_existing_what_ever.com',
|
||||
subject: 'some subject',
|
||||
|
@ -189,7 +189,7 @@ class EmailDeliverTest < ActiveSupport::TestCase
|
|||
# remove background jobs
|
||||
Delayed::Job.destroy_all
|
||||
|
||||
article2 = Ticket::Article.create(
|
||||
article2 = Ticket::Article.create!(
|
||||
ticket_id: ticket1.id,
|
||||
to: 'some_recipient@example_not_existing_what_ever.com',
|
||||
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',
|
||||
firstname: 'First',
|
||||
lastname: 'Last',
|
||||
email: 'some_geo_login1@example.com',
|
||||
email: 'some_geo_login2@example.com',
|
||||
password: 'test',
|
||||
street: 'Marienstrasse 13',
|
||||
city: 'Berlin',
|
||||
|
|
|
@ -10,7 +10,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
|||
login: 'admin',
|
||||
firstname: 'Bob',
|
||||
lastname: 'Smith',
|
||||
email: 'bob@example.com',
|
||||
email: 'bob+active_stream@example.com',
|
||||
password: 'some_pass',
|
||||
active: true,
|
||||
roles: roles,
|
||||
|
@ -23,7 +23,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test 'ticket+user' do
|
||||
ticket = Ticket.create(
|
||||
ticket = Ticket.create!(
|
||||
group_id: Group.lookup(name: 'Users').id,
|
||||
customer_id: @current_user.id,
|
||||
owner_id: User.lookup(login: '-').id,
|
||||
|
@ -35,7 +35,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
|||
)
|
||||
travel 2.seconds
|
||||
|
||||
article = Ticket::Article.create(
|
||||
article = Ticket::Article.create!(
|
||||
ticket_id: ticket.id,
|
||||
updated_by_id: @current_user.id,
|
||||
created_by_id: @current_user.id,
|
||||
|
@ -86,12 +86,12 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
|||
assert(stream.empty?)
|
||||
|
||||
# cleanup
|
||||
ticket.destroy
|
||||
ticket.destroy!
|
||||
travel_back
|
||||
end
|
||||
|
||||
test 'organization' do
|
||||
organization = Organization.create(
|
||||
organization = Organization.create!(
|
||||
name: 'some name',
|
||||
updated_by_id: @current_user.id,
|
||||
created_by_id: @current_user.id,
|
||||
|
@ -125,12 +125,12 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
|||
assert(stream.empty?)
|
||||
|
||||
# cleanup
|
||||
organization.destroy
|
||||
organization.destroy!
|
||||
travel_back
|
||||
end
|
||||
|
||||
test 'user with update check false' do
|
||||
user = User.create(
|
||||
user = User.create!(
|
||||
login: 'someemail@example.com',
|
||||
email: 'someemail@example.com',
|
||||
firstname: 'Bob Smith II',
|
||||
|
@ -157,12 +157,12 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
|||
assert(stream.empty?)
|
||||
|
||||
# cleanup
|
||||
user.destroy
|
||||
user.destroy!
|
||||
travel_back
|
||||
end
|
||||
|
||||
test 'user with update check true' do
|
||||
user = User.create(
|
||||
user = User.create!(
|
||||
login: 'someemail@example.com',
|
||||
email: 'someemail@example.com',
|
||||
firstname: 'Bob Smith II',
|
||||
|
@ -204,7 +204,7 @@ class ActivityStreamTest < ActiveSupport::TestCase
|
|||
assert(stream.empty?)
|
||||
|
||||
# cleanup
|
||||
user.destroy
|
||||
user.destroy!
|
||||
travel_back
|
||||
end
|
||||
|
||||
|
|
|
@ -22,6 +22,12 @@ class EmailBuildTest < ActiveSupport::TestCase
|
|||
assert(result !~ /font-family/, '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
|
||||
|
||||
test 'html email + attachment check' do
|
||||
|
|
|
@ -155,12 +155,12 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
|
||||
# use transaction
|
||||
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
|
||||
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(article.class.to_s, 'Ticket::Article')
|
||||
assert_equal(ticket.class, Ticket)
|
||||
assert_equal(article.class, Ticket::Article)
|
||||
|
||||
# update ticket
|
||||
if test[:ticket_update][:ticket]
|
||||
|
@ -185,25 +185,21 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
}
|
||||
|
||||
# delete tickets
|
||||
tickets.each { |ticket|
|
||||
ticket_id = ticket.id
|
||||
ticket.destroy
|
||||
found = Ticket.where(id: ticket_id).first
|
||||
assert_not(found, 'Ticket destroyed')
|
||||
}
|
||||
tickets.each(&:destroy!)
|
||||
end
|
||||
|
||||
test 'user' do
|
||||
name = rand(999_999)
|
||||
tests = [
|
||||
|
||||
# test 1
|
||||
{
|
||||
user_create: {
|
||||
user: {
|
||||
login: 'some_login_test',
|
||||
login: "some_login_test-#{name}",
|
||||
firstname: 'Bob',
|
||||
lastname: 'Smith',
|
||||
email: 'somebody@example.com',
|
||||
email: "somebody-#{name}@example.com",
|
||||
active: true,
|
||||
updated_by_id: current_user.id,
|
||||
created_by_id: current_user.id,
|
||||
|
@ -213,7 +209,7 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
user: {
|
||||
firstname: 'Bob',
|
||||
lastname: 'Master',
|
||||
email: 'master@example.com',
|
||||
email: "master-#{name}@example.com",
|
||||
active: false,
|
||||
},
|
||||
},
|
||||
|
@ -236,8 +232,8 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
history_object: 'User',
|
||||
history_type: 'updated',
|
||||
history_attribute: 'email',
|
||||
value_from: 'somebody@example.com',
|
||||
value_to: 'master@example.com',
|
||||
value_from: "somebody-#{name}@example.com",
|
||||
value_to: "master-#{name}@example.com",
|
||||
},
|
||||
{
|
||||
result: true,
|
||||
|
@ -258,9 +254,8 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
|
||||
# user transaction
|
||||
ActiveRecord::Base.transaction do
|
||||
user = User.create(test[:user_create][:user])
|
||||
|
||||
assert_equal(user.class.to_s, 'User')
|
||||
user = User.create!(test[:user_create][:user])
|
||||
assert_equal(user.class, User)
|
||||
|
||||
# update user
|
||||
if test[:user_update][:user]
|
||||
|
@ -277,12 +272,7 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
}
|
||||
|
||||
# delete user
|
||||
users.each { |user|
|
||||
user_id = user.id
|
||||
user.destroy
|
||||
found = User.where(id: user_id).first
|
||||
assert_not(found, 'User destroyed')
|
||||
}
|
||||
users.each(&:destroy!)
|
||||
end
|
||||
|
||||
test 'organization' do
|
||||
|
@ -328,9 +318,8 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
|
||||
# user transaction
|
||||
ActiveRecord::Base.transaction do
|
||||
organization = Organization.create(test[:organization_create][:organization])
|
||||
|
||||
assert_equal(organization.class.to_s, 'Organization')
|
||||
organization = Organization.create!(test[:organization_create][:organization])
|
||||
assert_equal(organization.class, Organization)
|
||||
|
||||
# update organization
|
||||
if test[:organization_update][:organization]
|
||||
|
@ -346,12 +335,7 @@ class HistoryTest < ActiveSupport::TestCase
|
|||
}
|
||||
|
||||
# delete user
|
||||
organizations.each { |organization|
|
||||
organization_id = organization.id
|
||||
organization.destroy
|
||||
found = Organization.where(id: organization_id).first
|
||||
assert_not(found, 'Organization destroyed')
|
||||
}
|
||||
organizations.each(&:destroy!)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
test 'bbb overiview index' do
|
||||
test 'bbb overview index' do
|
||||
|
||||
result = Ticket::Overviews.all(
|
||||
current_user: @agent1,
|
||||
|
@ -343,7 +343,7 @@ class TicketOverviewTest < ActiveSupport::TestCase
|
|||
|
||||
end
|
||||
|
||||
test 'ccc overiview content' do
|
||||
test 'ccc overview content' do
|
||||
|
||||
Ticket.destroy_all
|
||||
|
||||
|
|
|
@ -245,9 +245,9 @@ class UserTest < ActiveSupport::TestCase
|
|||
tests.each { |test|
|
||||
|
||||
# check if user exists
|
||||
user = User.where( login: test[:create][:login] ).first
|
||||
user = User.where(login: test[:create][:login]).first
|
||||
if user
|
||||
user.destroy
|
||||
user.destroy!
|
||||
end
|
||||
|
||||
user = User.create( test[:create] )
|
||||
|
@ -266,8 +266,8 @@ class UserTest < ActiveSupport::TestCase
|
|||
end
|
||||
}
|
||||
if test[:create_verify][:image_md5]
|
||||
file = Avatar.get_by_hash( user.image )
|
||||
file_md5 = Digest::MD5.hexdigest( file.content )
|
||||
file = Avatar.get_by_hash(user.image)
|
||||
file_md5 = Digest::MD5.hexdigest(file.content)
|
||||
assert_equal(test[:create_verify][:image_md5], file_md5, "create avatar md5 check in (#{test[:name]})")
|
||||
end
|
||||
if test[:update]
|
||||
|
@ -275,7 +275,7 @@ class UserTest < ActiveSupport::TestCase
|
|||
|
||||
test[:update_verify].each { |key, value|
|
||||
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]})")
|
||||
else
|
||||
assert_equal(value, user[key], "update check #{key} in (#{test[:name]})")
|
||||
|
@ -289,10 +289,252 @@ class UserTest < ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
user.destroy
|
||||
user.destroy!
|
||||
}
|
||||
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
|
||||
name = rand(999_999_999)
|
||||
groups = Group.where(name: 'Users')
|
||||
|
@ -352,7 +594,6 @@ class UserTest < ActiveSupport::TestCase
|
|||
assert(customer1.preferences['notification_config'])
|
||||
assert(customer1.preferences['notification_config']['matrix']['create'])
|
||||
assert(customer1.preferences['notification_config']['matrix']['update'])
|
||||
|
||||
end
|
||||
|
||||
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
|
||||
User.with_permissions('admin').each do |user|
|
||||
Models.merge('User', 1, user.id)
|
||||
user.destroy
|
||||
user.destroy!
|
||||
end
|
||||
|
||||
# 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