Fixed timing issues, improved tests.
This commit is contained in:
parent
465ab3e6be
commit
f06348e843
7 changed files with 409 additions and 274 deletions
|
@ -291,7 +291,7 @@ class ChatWindow extends App.Controller
|
|||
phrasesArray = phrases.split(';')
|
||||
phrase = phrasesArray[_.random(0, phrasesArray.length-1)]
|
||||
@input.html(phrase)
|
||||
@sendMessage()
|
||||
@sendMessage(1600)
|
||||
|
||||
focus: =>
|
||||
@input.focus()
|
||||
|
@ -372,16 +372,27 @@ class ChatWindow extends App.Controller
|
|||
event.preventDefault()
|
||||
@sendMessage()
|
||||
|
||||
sendMessage: =>
|
||||
sendMessage: (delay) =>
|
||||
content = @input.html()
|
||||
return if !content
|
||||
|
||||
App.WebSocket.send(
|
||||
event:'chat_session_message'
|
||||
data:
|
||||
content: content
|
||||
session_id: @session.session_id
|
||||
)
|
||||
send = =>
|
||||
App.WebSocket.send(
|
||||
event:'chat_session_message'
|
||||
data:
|
||||
content: content
|
||||
session_id: @session.session_id
|
||||
)
|
||||
if !delay
|
||||
send()
|
||||
else
|
||||
# show key enter and send phrase
|
||||
App.WebSocket.send(
|
||||
event:'chat_session_typing'
|
||||
data:
|
||||
session_id: @session.session_id
|
||||
)
|
||||
@delay(send, delay)
|
||||
|
||||
@addMessage content, 'agent'
|
||||
@input.html('')
|
||||
|
|
|
@ -73,7 +73,7 @@ do($ = window.jQuery, window) ->
|
|||
|
||||
stop: =>
|
||||
return if !@intervallId
|
||||
@log.debug "Stop timeout of #{@options.timeout} minutes at"#, new Date
|
||||
@log.debug "Stop timeout of #{@options.timeout} minutes"#, new Date
|
||||
clearInterval(@intervallId)
|
||||
|
||||
class Io extends Base
|
||||
|
@ -89,30 +89,40 @@ do($ = window.jQuery, window) ->
|
|||
@log.debug "Connecting to #{@options.host}"
|
||||
@ws = new window.WebSocket("#{@options.host}")
|
||||
@ws.onopen = (e) =>
|
||||
@log.debug 'on open', e
|
||||
@log.debug 'onOpen', e
|
||||
@options.onOpen(e)
|
||||
|
||||
@ws.onmessage = (e) =>
|
||||
pipes = JSON.parse(e.data)
|
||||
@log.debug 'on message', e.data
|
||||
@log.debug 'onMessage', e.data
|
||||
if @options.onMessage
|
||||
@options.onMessage(pipes)
|
||||
|
||||
@ws.onclose = (e) =>
|
||||
@log.debug 'close websocket connection'
|
||||
if @options.onClose
|
||||
@options.onClose(e)
|
||||
@log.debug 'close websocket connection', e
|
||||
if @manualClose
|
||||
@log.debug 'manual close, onClose callback'
|
||||
@manualClose = false
|
||||
if @options.onClose
|
||||
@options.onClose(e)
|
||||
else
|
||||
@log.debug 'error close, onError callback'
|
||||
if @options.onError
|
||||
@options.onError('Connection lost...')
|
||||
|
||||
@ws.onerror = (e) =>
|
||||
@log.debug 'onerror', e
|
||||
@log.debug 'onError', e
|
||||
if @options.onError
|
||||
@options.onError(e)
|
||||
|
||||
close: =>
|
||||
@log.debug 'close websocket manually'
|
||||
@manualClose = true
|
||||
@ws.close()
|
||||
|
||||
reconnect: =>
|
||||
@ws.close()
|
||||
@log.debug 'reconnect'
|
||||
@close()
|
||||
@connect()
|
||||
|
||||
send: (event, data = {}) =>
|
||||
|
@ -137,7 +147,7 @@ do($ = window.jQuery, window) ->
|
|||
buttonClass: 'open-zammad-chat'
|
||||
inactiveClass: 'is-inactive'
|
||||
title: '<strong>Chat</strong> with us!'
|
||||
idleTimeout: 8
|
||||
idleTimeout: 6
|
||||
idleTimeoutIntervallCheck: 0.5
|
||||
inactiveTimeout: 8
|
||||
inactiveTimeoutIntervallCheck: 0.5
|
||||
|
@ -146,7 +156,7 @@ do($ = window.jQuery, window) ->
|
|||
|
||||
logPrefix: 'chat'
|
||||
_messageCount: 0
|
||||
isOpen: true
|
||||
isOpen: false
|
||||
blinkOnlineInterval: null
|
||||
stopBlinOnlineStateTimeout: null
|
||||
showTimeEveryXMinutes: 1
|
||||
|
@ -232,31 +242,39 @@ do($ = window.jQuery, window) ->
|
|||
@io = new Io(@options)
|
||||
@io.set(
|
||||
onOpen: @render
|
||||
onClose: @hide
|
||||
onClose: @onWebSocketClose
|
||||
onMessage: @onWebSocketMessage
|
||||
onError: @onError
|
||||
)
|
||||
@io.connect()
|
||||
|
||||
render: =>
|
||||
@el = $(@view('chat')(
|
||||
title: @options.title
|
||||
))
|
||||
@options.target.append @el
|
||||
if !@el || !$('.zammad-chat').get(0)
|
||||
@el = $(@view('chat')(
|
||||
title: @options.title
|
||||
))
|
||||
@options.target.append @el
|
||||
|
||||
@input = @el.find('.zammad-chat-input')
|
||||
@input = @el.find('.zammad-chat-input')
|
||||
|
||||
# start bindings
|
||||
@el.find('.js-chat-open').click @open
|
||||
@el.find('.js-chat-close').click @close
|
||||
@el.find('.zammad-chat-controls').on 'submit', @onSubmit
|
||||
@input.on
|
||||
keydown: @checkForEnter
|
||||
input: @onInput
|
||||
$(window).on('beforeunload', =>
|
||||
@onLeaveTemporary()
|
||||
)
|
||||
$(window).bind('hashchange', =>
|
||||
return if @isOpen
|
||||
@idleTimeout.start()
|
||||
)
|
||||
|
||||
# disable open button
|
||||
$(".#{ @options.buttonClass }").addClass @inactiveClass
|
||||
|
||||
@el.find('.js-chat-open').click @open
|
||||
@el.find('.js-chat-close').click @close
|
||||
@el.find('.zammad-chat-controls').on 'submit', @onSubmit
|
||||
@input.on
|
||||
keydown: @checkForEnter
|
||||
input: @onInput
|
||||
$(window).on('beforeunload', =>
|
||||
@onLeaveTemporary()
|
||||
)
|
||||
@setAgentOnlineState 'online'
|
||||
|
||||
@log.debug 'widget rendered'
|
||||
|
@ -285,7 +303,7 @@ do($ = window.jQuery, window) ->
|
|||
when 'chat_error'
|
||||
@log.notice pipe.data
|
||||
if pipe.data && pipe.data.state is 'chat_disabled'
|
||||
@destroy(hide: true)
|
||||
@destroy(remove: true)
|
||||
when 'chat_session_message'
|
||||
return if pipe.data.self_written
|
||||
@receiveMessage pipe.data
|
||||
|
@ -307,21 +325,14 @@ do($ = window.jQuery, window) ->
|
|||
@onReady()
|
||||
when 'offline'
|
||||
@onError 'Zammad Chat: No agent online'
|
||||
@state = 'off'
|
||||
@destroy(hide: true)
|
||||
when 'chat_disabled'
|
||||
@onError 'Zammad Chat: Chat is disabled'
|
||||
@state = 'off'
|
||||
@destroy(hide: true)
|
||||
when 'no_seats_available'
|
||||
@onError "Zammad Chat: Too many clients in queue. Clients in queue: #{pipe.data.queue}"
|
||||
@state = 'off'
|
||||
@destroy(hide: true)
|
||||
when 'reconnect'
|
||||
@log.debug 'old messages', pipe.data.session
|
||||
@reopenSession pipe.data
|
||||
@onReopenSession pipe.data
|
||||
|
||||
onReady: =>
|
||||
onReady: ->
|
||||
@log.debug 'widget ready for use'
|
||||
$(".#{ @options.buttonClass }").click(@open).removeClass(@inactiveClass)
|
||||
|
||||
|
@ -330,9 +341,16 @@ do($ = window.jQuery, window) ->
|
|||
|
||||
onError: (message) =>
|
||||
@log.debug message
|
||||
@addStatus(message)
|
||||
$(".#{ @options.buttonClass }").hide()
|
||||
if @isOpen
|
||||
@disableInput()
|
||||
@destroy(remove: false)
|
||||
else
|
||||
@destroy(remove: true)
|
||||
|
||||
reopenSession: (data) =>
|
||||
onReopenSession: (data) =>
|
||||
@log.debug 'old messages', data.session
|
||||
@inactiveTimeout.start()
|
||||
|
||||
unfinishedMessage = sessionStorage.getItem 'unfinished_message'
|
||||
|
@ -436,9 +454,12 @@ do($ = window.jQuery, window) ->
|
|||
@scrollToBottom()
|
||||
|
||||
open: =>
|
||||
@log.debug 'open widget'
|
||||
if @isOpen
|
||||
@show()
|
||||
@log.debug 'widget already open, block'
|
||||
return
|
||||
|
||||
@isOpen = true
|
||||
@log.debug 'open widget'
|
||||
|
||||
if !@sessionId
|
||||
@showLoader()
|
||||
|
@ -447,27 +468,15 @@ do($ = window.jQuery, window) ->
|
|||
|
||||
if !@sessionId
|
||||
@el.animate { bottom: 0 }, 500, @onOpenAnimationEnd
|
||||
@send('chat_session_init')
|
||||
else
|
||||
@el.css 'bottom', 0
|
||||
@onOpenAnimationEnd()
|
||||
|
||||
@isOpen = true
|
||||
|
||||
if !@sessionId
|
||||
@send('chat_session_init')
|
||||
|
||||
onOpenAnimationEnd: =>
|
||||
@idleTimeout.stop()
|
||||
|
||||
close: (event) =>
|
||||
@log.debug 'close widget'
|
||||
|
||||
return @state if @state is 'off' or @state is 'unsupported'
|
||||
event.stopPropagation() if event
|
||||
|
||||
# only close if session_id exists
|
||||
return if !@sessionId
|
||||
|
||||
sessionClose: =>
|
||||
# send close
|
||||
@send 'chat_session_close',
|
||||
session_id: @sessionId
|
||||
|
@ -483,12 +492,23 @@ do($ = window.jQuery, window) ->
|
|||
if @onInitialQueueDelayId
|
||||
clearTimeout(@onInitialQueueDelayId)
|
||||
|
||||
if event
|
||||
@closeWindow()
|
||||
|
||||
@setSessionId undefined
|
||||
|
||||
closeWindow: =>
|
||||
close: (event) =>
|
||||
if !@isOpen
|
||||
@log.debug 'can\'t close widget, it\'s not open'
|
||||
return
|
||||
if !@sessionId
|
||||
@log.debug 'can\'t close widget without sessionId'
|
||||
return
|
||||
|
||||
@log.debug 'close widget'
|
||||
|
||||
event.stopPropagation() if event
|
||||
|
||||
@sessionClose()
|
||||
|
||||
# close window
|
||||
@el.removeClass('zammad-chat-is-open')
|
||||
remainerHeight = @el.height() - @el.find('.zammad-chat-header').outerHeight()
|
||||
@el.animate { bottom: -remainerHeight }, 500, @onCloseAnimationEnd
|
||||
|
@ -503,15 +523,15 @@ do($ = window.jQuery, window) ->
|
|||
|
||||
@isOpen = false
|
||||
|
||||
# restart connection
|
||||
@io.reconnect()
|
||||
|
||||
hide: ->
|
||||
onWebSocketClose: =>
|
||||
return if @isOpen
|
||||
if @el
|
||||
@el.removeClass('zammad-chat-is-shown')
|
||||
|
||||
show: ->
|
||||
return @state if @state is 'off' or @state is 'unsupported'
|
||||
return if @state is 'offline'
|
||||
|
||||
@el.addClass('zammad-chat-is-shown')
|
||||
|
||||
|
@ -600,6 +620,7 @@ do($ = window.jQuery, window) ->
|
|||
@scrollToBottom()
|
||||
|
||||
updateLastTimestamp: (label, time) ->
|
||||
return if !@el
|
||||
@el.find('.zammad-chat-body')
|
||||
.find('.zammad-chat-timestamp')
|
||||
.last()
|
||||
|
@ -608,6 +629,7 @@ do($ = window.jQuery, window) ->
|
|||
time: time
|
||||
|
||||
addStatus: (status) ->
|
||||
return if !@el
|
||||
@maybeAddTimestamp()
|
||||
|
||||
@el.find('.zammad-chat-body').append @view('status')
|
||||
|
@ -619,30 +641,24 @@ do($ = window.jQuery, window) ->
|
|||
@el.find('.zammad-chat-body').scrollTop($('.zammad-chat-body').prop('scrollHeight'))
|
||||
|
||||
destroy: (params = {}) =>
|
||||
@log.debug 'destroy widget'
|
||||
if params.hide
|
||||
if @el
|
||||
@el.remove()
|
||||
@log.debug 'destroy widget', params
|
||||
|
||||
@setAgentOnlineState 'offline'
|
||||
|
||||
if params.remove && @el
|
||||
@el.remove()
|
||||
|
||||
# stop all timer
|
||||
@waitingListTimeout.stop()
|
||||
@inactiveTimeout.stop()
|
||||
@idleTimeout.stop()
|
||||
@wsReconnectStop()
|
||||
if @waitingListTimeout
|
||||
@waitingListTimeout.stop()
|
||||
if @inactiveTimeout
|
||||
@inactiveTimeout.stop()
|
||||
if @idleTimeout
|
||||
@idleTimeout.stop()
|
||||
|
||||
# stop ws connection
|
||||
@io.close()
|
||||
|
||||
wsReconnectStart: =>
|
||||
@wsReconnectStop()
|
||||
if @reconnectDelayId
|
||||
clearTimeout(@reconnectDelayId)
|
||||
@reconnectDelayId = setTimeout(@io.connect(), 5000)
|
||||
|
||||
wsReconnectStop: =>
|
||||
if @reconnectDelayId
|
||||
clearTimeout(@reconnectDelayId)
|
||||
|
||||
reconnect: =>
|
||||
# set status to connecting
|
||||
@log.notice 'reconnecting'
|
||||
|
@ -702,24 +718,25 @@ do($ = window.jQuery, window) ->
|
|||
@el.find('.zammad-chat-body').html @view('customer_timeout')
|
||||
agent: @agent.name
|
||||
delay: @options.inactiveTimeout
|
||||
@close()
|
||||
reload = ->
|
||||
location.reload()
|
||||
@el.find('.js-restart').click reload
|
||||
@sessionClose()
|
||||
|
||||
showWaitingListTimeout: ->
|
||||
@el.find('.zammad-chat-body').html @view('waiting_list_timeout')
|
||||
delay: @options.watingListTimeout
|
||||
@close()
|
||||
reload = ->
|
||||
location.reload()
|
||||
@el.find('.js-restart').click reload
|
||||
@sessionClose()
|
||||
|
||||
showLoader: ->
|
||||
@el.find('.zammad-chat-body').html @view('loader')()
|
||||
|
||||
setAgentOnlineState: (state) =>
|
||||
@state = state
|
||||
return if !@el
|
||||
capitalizedState = state.charAt(0).toUpperCase() + state.slice(1)
|
||||
@el
|
||||
.find('.zammad-chat-agent-status')
|
||||
|
@ -757,8 +774,7 @@ do($ = window.jQuery, window) ->
|
|||
timeoutIntervallCheck: @options.idleTimeoutIntervallCheck
|
||||
callback: =>
|
||||
@log.debug 'Idle timeout reached, hide widget', new Date
|
||||
@state = 'off'
|
||||
@destroy(hide: true)
|
||||
@destroy(remove: true)
|
||||
)
|
||||
@inactiveTimeout = new Timeout(
|
||||
logPrefix: 'inactiveTimeout'
|
||||
|
@ -767,10 +783,8 @@ do($ = window.jQuery, window) ->
|
|||
timeoutIntervallCheck: @options.inactiveTimeoutIntervallCheck
|
||||
callback: =>
|
||||
@log.debug 'Inactive timeout reached, show timeout screen.', new Date
|
||||
@state = 'off'
|
||||
@setAgentOnlineState 'offline'
|
||||
@showCustomerTimeout()
|
||||
@destroy(hide:false)
|
||||
@destroy(remove: false)
|
||||
)
|
||||
@waitingListTimeout = new Timeout(
|
||||
logPrefix: 'waitingListTimeout'
|
||||
|
@ -779,10 +793,8 @@ do($ = window.jQuery, window) ->
|
|||
timeoutIntervallCheck: @options.waitingListTimeoutIntervallCheck
|
||||
callback: =>
|
||||
@log.debug 'Waiting list timeout reached, show timeout screen.', new Date
|
||||
@state = 'off'
|
||||
@setAgentOnlineState 'offline'
|
||||
@showWaitingListTimeout()
|
||||
@destroy(hide:false)
|
||||
@destroy(remove: false)
|
||||
)
|
||||
|
||||
window.ZammadChat = ZammadChat
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
display: none;
|
||||
-webkit-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column; }
|
||||
flex-direction: column;
|
||||
z-index: 999; }
|
||||
.zammad-chat.is-fullscreen {
|
||||
right: 0;
|
||||
width: 100%;
|
||||
|
@ -106,7 +107,7 @@
|
|||
margin: 0 1em;
|
||||
display: inline-block;
|
||||
line-height: 2em;
|
||||
padding: 0 0.7em;
|
||||
padding: 0 .7em;
|
||||
border-radius: 1em;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.04) inset; }
|
||||
|
@ -349,6 +350,10 @@
|
|||
.zammad-chat-send {
|
||||
float: right; }
|
||||
|
||||
.zammad-chat-button:disabled,
|
||||
.zammad-chat-input:disabled {
|
||||
opacity: 0.3; }
|
||||
|
||||
.zammad-chat-is-hidden {
|
||||
display: none; }
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
if (!this.intervallId) {
|
||||
return;
|
||||
}
|
||||
this.log.debug("Stop timeout of " + this.options.timeout + " minutes at");
|
||||
this.log.debug("Stop timeout of " + this.options.timeout + " minutes");
|
||||
return clearInterval(this.intervallId);
|
||||
};
|
||||
|
||||
|
@ -164,7 +164,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.ws = new window.WebSocket("" + this.options.host);
|
||||
this.ws.onopen = (function(_this) {
|
||||
return function(e) {
|
||||
_this.log.debug('on open', e);
|
||||
_this.log.debug('onOpen', e);
|
||||
return _this.options.onOpen(e);
|
||||
};
|
||||
})(this);
|
||||
|
@ -172,7 +172,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
return function(e) {
|
||||
var pipes;
|
||||
pipes = JSON.parse(e.data);
|
||||
_this.log.debug('on message', e.data);
|
||||
_this.log.debug('onMessage', e.data);
|
||||
if (_this.options.onMessage) {
|
||||
return _this.options.onMessage(pipes);
|
||||
}
|
||||
|
@ -180,15 +180,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
})(this);
|
||||
this.ws.onclose = (function(_this) {
|
||||
return function(e) {
|
||||
_this.log.debug('close websocket connection');
|
||||
if (_this.options.onClose) {
|
||||
return _this.options.onClose(e);
|
||||
_this.log.debug('close websocket connection', e);
|
||||
if (_this.manualClose) {
|
||||
_this.log.debug('manual close, onClose callback');
|
||||
_this.manualClose = false;
|
||||
if (_this.options.onClose) {
|
||||
return _this.options.onClose(e);
|
||||
}
|
||||
} else {
|
||||
_this.log.debug('error close, onError callback');
|
||||
if (_this.options.onError) {
|
||||
return _this.options.onError('Connection lost...');
|
||||
}
|
||||
}
|
||||
};
|
||||
})(this);
|
||||
return this.ws.onerror = (function(_this) {
|
||||
return function(e) {
|
||||
_this.log.debug('onerror', e);
|
||||
_this.log.debug('onError', e);
|
||||
if (_this.options.onError) {
|
||||
return _this.options.onError(e);
|
||||
}
|
||||
|
@ -197,11 +206,14 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
Io.prototype.close = function() {
|
||||
this.log.debug('close websocket manually');
|
||||
this.manualClose = true;
|
||||
return this.ws.close();
|
||||
};
|
||||
|
||||
Io.prototype.reconnect = function() {
|
||||
this.ws.close();
|
||||
this.log.debug('reconnect');
|
||||
this.close();
|
||||
return this.connect();
|
||||
};
|
||||
|
||||
|
@ -238,7 +250,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
buttonClass: 'open-zammad-chat',
|
||||
inactiveClass: 'is-inactive',
|
||||
title: '<strong>Chat</strong> with us!',
|
||||
idleTimeout: 8,
|
||||
idleTimeout: 6,
|
||||
idleTimeoutIntervallCheck: 0.5,
|
||||
inactiveTimeout: 8,
|
||||
inactiveTimeoutIntervallCheck: 0.5,
|
||||
|
@ -250,7 +262,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
|
||||
ZammadChat.prototype._messageCount = 0;
|
||||
|
||||
ZammadChat.prototype.isOpen = true;
|
||||
ZammadChat.prototype.isOpen = false;
|
||||
|
||||
ZammadChat.prototype.blinkOnlineInterval = null;
|
||||
|
||||
|
@ -336,26 +348,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.setSessionId = bind(this.setSessionId, this);
|
||||
this.onConnectionReestablished = bind(this.onConnectionReestablished, this);
|
||||
this.reconnect = bind(this.reconnect, this);
|
||||
this.wsReconnectStop = bind(this.wsReconnectStop, this);
|
||||
this.wsReconnectStart = bind(this.wsReconnectStart, this);
|
||||
this.destroy = bind(this.destroy, this);
|
||||
this.onLeaveTemporary = bind(this.onLeaveTemporary, this);
|
||||
this.onAgentTypingEnd = bind(this.onAgentTypingEnd, this);
|
||||
this.onAgentTypingStart = bind(this.onAgentTypingStart, this);
|
||||
this.onQueue = bind(this.onQueue, this);
|
||||
this.onQueueScreen = bind(this.onQueueScreen, this);
|
||||
this.onWebSocketClose = bind(this.onWebSocketClose, this);
|
||||
this.onCloseAnimationEnd = bind(this.onCloseAnimationEnd, this);
|
||||
this.closeWindow = bind(this.closeWindow, this);
|
||||
this.close = bind(this.close, this);
|
||||
this.sessionClose = bind(this.sessionClose, this);
|
||||
this.onOpenAnimationEnd = bind(this.onOpenAnimationEnd, this);
|
||||
this.open = bind(this.open, this);
|
||||
this.renderMessage = bind(this.renderMessage, this);
|
||||
this.receiveMessage = bind(this.receiveMessage, this);
|
||||
this.onSubmit = bind(this.onSubmit, this);
|
||||
this.onInput = bind(this.onInput, this);
|
||||
this.reopenSession = bind(this.reopenSession, this);
|
||||
this.onReopenSession = bind(this.onReopenSession, this);
|
||||
this.onError = bind(this.onError, this);
|
||||
this.onReady = bind(this.onReady, this);
|
||||
this.onWebSocketMessage = bind(this.onWebSocketMessage, this);
|
||||
this.send = bind(this.send, this);
|
||||
this.checkForEnter = bind(this.checkForEnter, this);
|
||||
|
@ -393,31 +403,42 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.io = new Io(this.options);
|
||||
this.io.set({
|
||||
onOpen: this.render,
|
||||
onClose: this.hide,
|
||||
onMessage: this.onWebSocketMessage
|
||||
onClose: this.onWebSocketClose,
|
||||
onMessage: this.onWebSocketMessage,
|
||||
onError: this.onError
|
||||
});
|
||||
this.io.connect();
|
||||
}
|
||||
|
||||
ZammadChat.prototype.render = function() {
|
||||
this.el = $(this.view('chat')({
|
||||
title: this.options.title
|
||||
}));
|
||||
this.options.target.append(this.el);
|
||||
this.input = this.el.find('.zammad-chat-input');
|
||||
if (!this.el || !$('.zammad-chat').get(0)) {
|
||||
this.el = $(this.view('chat')({
|
||||
title: this.options.title
|
||||
}));
|
||||
this.options.target.append(this.el);
|
||||
this.input = this.el.find('.zammad-chat-input');
|
||||
this.el.find('.js-chat-open').click(this.open);
|
||||
this.el.find('.js-chat-close').click(this.close);
|
||||
this.el.find('.zammad-chat-controls').on('submit', this.onSubmit);
|
||||
this.input.on({
|
||||
keydown: this.checkForEnter,
|
||||
input: this.onInput
|
||||
});
|
||||
$(window).on('beforeunload', (function(_this) {
|
||||
return function() {
|
||||
return _this.onLeaveTemporary();
|
||||
};
|
||||
})(this));
|
||||
$(window).bind('hashchange', (function(_this) {
|
||||
return function() {
|
||||
if (_this.isOpen) {
|
||||
return;
|
||||
}
|
||||
return _this.idleTimeout.start();
|
||||
};
|
||||
})(this));
|
||||
}
|
||||
$("." + this.options.buttonClass).addClass(this.inactiveClass);
|
||||
this.el.find('.js-chat-open').click(this.open);
|
||||
this.el.find('.js-chat-close').click(this.close);
|
||||
this.el.find('.zammad-chat-controls').on('submit', this.onSubmit);
|
||||
this.input.on({
|
||||
keydown: this.checkForEnter,
|
||||
input: this.onInput
|
||||
});
|
||||
$(window).on('beforeunload', (function(_this) {
|
||||
return function() {
|
||||
return _this.onLeaveTemporary();
|
||||
};
|
||||
})(this));
|
||||
this.setAgentOnlineState('online');
|
||||
this.log.debug('widget rendered');
|
||||
this.startTimeoutObservers();
|
||||
|
@ -453,7 +474,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.log.notice(pipe.data);
|
||||
if (pipe.data && pipe.data.state === 'chat_disabled') {
|
||||
this.destroy({
|
||||
hide: true
|
||||
remove: true
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
@ -489,28 +510,15 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
break;
|
||||
case 'offline':
|
||||
this.onError('Zammad Chat: No agent online');
|
||||
this.state = 'off';
|
||||
this.destroy({
|
||||
hide: true
|
||||
});
|
||||
break;
|
||||
case 'chat_disabled':
|
||||
this.onError('Zammad Chat: Chat is disabled');
|
||||
this.state = 'off';
|
||||
this.destroy({
|
||||
hide: true
|
||||
});
|
||||
break;
|
||||
case 'no_seats_available':
|
||||
this.onError("Zammad Chat: Too many clients in queue. Clients in queue: " + pipe.data.queue);
|
||||
this.state = 'off';
|
||||
this.destroy({
|
||||
hide: true
|
||||
});
|
||||
break;
|
||||
case 'reconnect':
|
||||
this.log.debug('old messages', pipe.data.session);
|
||||
this.reopenSession(pipe.data);
|
||||
this.onReopenSession(pipe.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -526,11 +534,23 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
|
||||
ZammadChat.prototype.onError = function(message) {
|
||||
this.log.debug(message);
|
||||
return $("." + this.options.buttonClass).hide();
|
||||
this.addStatus(message);
|
||||
$("." + this.options.buttonClass).hide();
|
||||
if (this.isOpen) {
|
||||
this.disableInput();
|
||||
return this.destroy({
|
||||
remove: false
|
||||
});
|
||||
} else {
|
||||
return this.destroy({
|
||||
remove: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ZammadChat.prototype.reopenSession = function(data) {
|
||||
ZammadChat.prototype.onReopenSession = function(data) {
|
||||
var i, len, message, ref, unfinishedMessage;
|
||||
this.log.debug('old messages', data.session);
|
||||
this.inactiveTimeout.start();
|
||||
unfinishedMessage = sessionStorage.getItem('unfinished_message');
|
||||
if (data.agent) {
|
||||
|
@ -631,10 +651,12 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
ZammadChat.prototype.open = function() {
|
||||
this.log.debug('open widget');
|
||||
if (this.isOpen) {
|
||||
this.show();
|
||||
this.log.debug('widget already open, block');
|
||||
return;
|
||||
}
|
||||
this.isOpen = true;
|
||||
this.log.debug('open widget');
|
||||
if (!this.sessionId) {
|
||||
this.showLoader();
|
||||
}
|
||||
|
@ -643,13 +665,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.el.animate({
|
||||
bottom: 0
|
||||
}, 500, this.onOpenAnimationEnd);
|
||||
return this.send('chat_session_init');
|
||||
} else {
|
||||
this.el.css('bottom', 0);
|
||||
this.onOpenAnimationEnd();
|
||||
}
|
||||
this.isOpen = true;
|
||||
if (!this.sessionId) {
|
||||
return this.send('chat_session_init');
|
||||
return this.onOpenAnimationEnd();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -657,17 +676,7 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
return this.idleTimeout.stop();
|
||||
};
|
||||
|
||||
ZammadChat.prototype.close = function(event) {
|
||||
this.log.debug('close widget');
|
||||
if (this.state === 'off' || this.state === 'unsupported') {
|
||||
return this.state;
|
||||
}
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
if (!this.sessionId) {
|
||||
return;
|
||||
}
|
||||
ZammadChat.prototype.sessionClose = function() {
|
||||
this.send('chat_session_close', {
|
||||
session_id: this.sessionId
|
||||
});
|
||||
|
@ -677,14 +686,24 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
if (this.onInitialQueueDelayId) {
|
||||
clearTimeout(this.onInitialQueueDelayId);
|
||||
}
|
||||
if (event) {
|
||||
this.closeWindow();
|
||||
}
|
||||
return this.setSessionId(void 0);
|
||||
};
|
||||
|
||||
ZammadChat.prototype.closeWindow = function() {
|
||||
ZammadChat.prototype.close = function(event) {
|
||||
var remainerHeight;
|
||||
if (!this.isOpen) {
|
||||
this.log.debug('can\'t close widget, it\'s not open');
|
||||
return;
|
||||
}
|
||||
if (!this.sessionId) {
|
||||
this.log.debug('can\'t close widget without sessionId');
|
||||
return;
|
||||
}
|
||||
this.log.debug('close widget');
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
this.sessionClose();
|
||||
this.el.removeClass('zammad-chat-is-open');
|
||||
remainerHeight = this.el.height() - this.el.find('.zammad-chat-header').outerHeight();
|
||||
return this.el.animate({
|
||||
|
@ -702,7 +721,10 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
return this.io.reconnect();
|
||||
};
|
||||
|
||||
ZammadChat.prototype.hide = function() {
|
||||
ZammadChat.prototype.onWebSocketClose = function() {
|
||||
if (this.isOpen) {
|
||||
return;
|
||||
}
|
||||
if (this.el) {
|
||||
return this.el.removeClass('zammad-chat-is-shown');
|
||||
}
|
||||
|
@ -710,8 +732,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
|
||||
ZammadChat.prototype.show = function() {
|
||||
var remainerHeight;
|
||||
if (this.state === 'off' || this.state === 'unsupported') {
|
||||
return this.state;
|
||||
if (this.state === 'offline') {
|
||||
return;
|
||||
}
|
||||
this.el.addClass('zammad-chat-is-shown');
|
||||
if (!this.inputInitialized) {
|
||||
|
@ -809,6 +831,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
ZammadChat.prototype.updateLastTimestamp = function(label, time) {
|
||||
if (!this.el) {
|
||||
return;
|
||||
}
|
||||
return this.el.find('.zammad-chat-body').find('.zammad-chat-timestamp').last().replaceWith(this.view('timestamp')({
|
||||
label: label,
|
||||
time: time
|
||||
|
@ -816,6 +841,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
};
|
||||
|
||||
ZammadChat.prototype.addStatus = function(status) {
|
||||
if (!this.el) {
|
||||
return;
|
||||
}
|
||||
this.maybeAddTimestamp();
|
||||
this.el.find('.zammad-chat-body').append(this.view('status')({
|
||||
status: status
|
||||
|
@ -831,33 +859,23 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
if (params == null) {
|
||||
params = {};
|
||||
}
|
||||
this.log.debug('destroy widget');
|
||||
if (params.hide) {
|
||||
if (this.el) {
|
||||
this.el.remove();
|
||||
}
|
||||
this.log.debug('destroy widget', params);
|
||||
this.setAgentOnlineState('offline');
|
||||
if (params.remove && this.el) {
|
||||
this.el.remove();
|
||||
}
|
||||
if (this.waitingListTimeout) {
|
||||
this.waitingListTimeout.stop();
|
||||
}
|
||||
if (this.inactiveTimeout) {
|
||||
this.inactiveTimeout.stop();
|
||||
}
|
||||
if (this.idleTimeout) {
|
||||
this.idleTimeout.stop();
|
||||
}
|
||||
this.waitingListTimeout.stop();
|
||||
this.inactiveTimeout.stop();
|
||||
this.idleTimeout.stop();
|
||||
this.wsReconnectStop();
|
||||
return this.io.close();
|
||||
};
|
||||
|
||||
ZammadChat.prototype.wsReconnectStart = function() {
|
||||
this.wsReconnectStop();
|
||||
if (this.reconnectDelayId) {
|
||||
clearTimeout(this.reconnectDelayId);
|
||||
}
|
||||
return this.reconnectDelayId = setTimeout(this.io.connect(), 5000);
|
||||
};
|
||||
|
||||
ZammadChat.prototype.wsReconnectStop = function() {
|
||||
if (this.reconnectDelayId) {
|
||||
return clearTimeout(this.reconnectDelayId);
|
||||
}
|
||||
};
|
||||
|
||||
ZammadChat.prototype.reconnect = function() {
|
||||
this.log.notice('reconnecting');
|
||||
this.disableInput();
|
||||
|
@ -920,11 +938,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
agent: this.agent.name,
|
||||
delay: this.options.inactiveTimeout
|
||||
}));
|
||||
this.close();
|
||||
reload = function() {
|
||||
return location.reload();
|
||||
};
|
||||
return this.el.find('.js-restart').click(reload);
|
||||
this.el.find('.js-restart').click(reload);
|
||||
return this.sessionClose();
|
||||
};
|
||||
|
||||
ZammadChat.prototype.showWaitingListTimeout = function() {
|
||||
|
@ -932,11 +950,11 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
this.el.find('.zammad-chat-body').html(this.view('waiting_list_timeout')({
|
||||
delay: this.options.watingListTimeout
|
||||
}));
|
||||
this.close();
|
||||
reload = function() {
|
||||
return location.reload();
|
||||
};
|
||||
return this.el.find('.js-restart').click(reload);
|
||||
this.el.find('.js-restart').click(reload);
|
||||
return this.sessionClose();
|
||||
};
|
||||
|
||||
ZammadChat.prototype.showLoader = function() {
|
||||
|
@ -946,6 +964,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
ZammadChat.prototype.setAgentOnlineState = function(state) {
|
||||
var capitalizedState;
|
||||
this.state = state;
|
||||
if (!this.el) {
|
||||
return;
|
||||
}
|
||||
capitalizedState = state.charAt(0).toUpperCase() + state.slice(1);
|
||||
return this.el.find('.zammad-chat-agent-status').attr('data-status', state).text(this.T(capitalizedState));
|
||||
};
|
||||
|
@ -986,9 +1007,8 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
callback: (function(_this) {
|
||||
return function() {
|
||||
_this.log.debug('Idle timeout reached, hide widget', new Date);
|
||||
_this.state = 'off';
|
||||
return _this.destroy({
|
||||
hide: true
|
||||
remove: true
|
||||
});
|
||||
};
|
||||
})(this)
|
||||
|
@ -1001,11 +1021,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
callback: (function(_this) {
|
||||
return function() {
|
||||
_this.log.debug('Inactive timeout reached, show timeout screen.', new Date);
|
||||
_this.state = 'off';
|
||||
_this.setAgentOnlineState('offline');
|
||||
_this.showCustomerTimeout();
|
||||
return _this.destroy({
|
||||
hide: false
|
||||
remove: false
|
||||
});
|
||||
};
|
||||
})(this)
|
||||
|
@ -1018,11 +1036,9 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
callback: (function(_this) {
|
||||
return function() {
|
||||
_this.log.debug('Waiting list timeout reached, show timeout screen.', new Date);
|
||||
_this.state = 'off';
|
||||
_this.setAgentOnlineState('offline');
|
||||
_this.showWaitingListTimeout();
|
||||
return _this.destroy({
|
||||
hide: false
|
||||
remove: false
|
||||
});
|
||||
};
|
||||
})(this)
|
||||
|
@ -1035,67 +1051,6 @@ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments);
|
|||
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):
|
||||
|
@ -1181,6 +1136,67 @@ jQuery.fn.autoGrow = function(options) {
|
|||
|
||||
});
|
||||
};
|
||||
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('');
|
||||
};
|
||||
|
||||
if (!window.zammadChatTemplates) {
|
||||
window.zammadChatTemplates = {};
|
||||
}
|
||||
|
|
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
|
@ -11,6 +11,7 @@
|
|||
will-change: bottom;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
z-index: 999;
|
||||
|
||||
&.is-fullscreen {
|
||||
right: 0;
|
||||
|
@ -351,6 +352,11 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
.zammad-chat-button:disabled,
|
||||
.zammad-chat-input:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.zammad-chat-is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -387,6 +387,42 @@ class ChatTest < TestCase
|
|||
css: '.zammad-chat',
|
||||
value: 'Chat closed by',
|
||||
)
|
||||
click(
|
||||
browser: customer,
|
||||
css: '.zammad-chat .js-chat-close',
|
||||
)
|
||||
watch_for_disappear(
|
||||
browser: customer,
|
||||
css: '.zammad-chat-is-open',
|
||||
)
|
||||
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
|
||||
sleep 2
|
||||
click(
|
||||
browser: customer,
|
||||
css: '.js-chat-open',
|
||||
)
|
||||
exists(
|
||||
browser: customer,
|
||||
css: '.zammad-chat-is-shown',
|
||||
)
|
||||
watch_for(
|
||||
browser: customer,
|
||||
css: '.zammad-chat',
|
||||
value: '(waiting|warte)',
|
||||
)
|
||||
click(
|
||||
browser: agent,
|
||||
css: '.active .js-acceptChat',
|
||||
)
|
||||
sleep 2
|
||||
exists_not(
|
||||
browser: agent,
|
||||
css: '.active .chat-window .chat-status.is-modified',
|
||||
)
|
||||
exists(
|
||||
browser: agent,
|
||||
css: '.active .chat-window .chat-status',
|
||||
)
|
||||
end
|
||||
|
||||
def test_basic_usecase3
|
||||
|
@ -579,7 +615,7 @@ class ChatTest < TestCase
|
|||
browser: customer,
|
||||
css: '.zammad-chat',
|
||||
value: '(takes longer|dauert länger)',
|
||||
timeout: 90,
|
||||
timeout: 120,
|
||||
)
|
||||
|
||||
# no customer action, show sorry screen
|
||||
|
@ -625,6 +661,55 @@ class ChatTest < TestCase
|
|||
timeout: 150,
|
||||
)
|
||||
|
||||
agent.find_elements( { css: '.active .chat-window .js-close' } ).each(&:click)
|
||||
sleep 2
|
||||
click(
|
||||
browser: customer,
|
||||
css: '.js-restart',
|
||||
)
|
||||
sleep 5
|
||||
click(
|
||||
browser: customer,
|
||||
css: '.js-chat-open',
|
||||
)
|
||||
exists(
|
||||
browser: customer,
|
||||
css: '.zammad-chat-is-shown',
|
||||
)
|
||||
watch_for(
|
||||
browser: customer,
|
||||
css: '.zammad-chat',
|
||||
value: '(waiting|warte)',
|
||||
)
|
||||
click(
|
||||
browser: agent,
|
||||
css: '.active .js-acceptChat',
|
||||
)
|
||||
sleep 2
|
||||
exists(
|
||||
browser: agent,
|
||||
css: '.active .chat-window .chat-status',
|
||||
)
|
||||
set(
|
||||
browser: agent,
|
||||
css: '.active .chat-window .js-customerChatInput',
|
||||
value: 'my name is me',
|
||||
)
|
||||
click(
|
||||
browser: agent,
|
||||
css: '.active .chat-window .js-send',
|
||||
)
|
||||
watch_for(
|
||||
browser: customer,
|
||||
css: '.zammad-chat .zammad-chat-agent-status',
|
||||
value: 'online',
|
||||
)
|
||||
watch_for(
|
||||
browser: customer,
|
||||
css: '.zammad-chat',
|
||||
value: 'my name is me',
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue