Merge branch 'develop' of github.com:martini/zammad into develop

This commit is contained in:
rkaldung 2015-01-24 21:25:41 +01:00
commit 4b1cfafc01
25 changed files with 888 additions and 1025 deletions

View file

@ -467,6 +467,31 @@ class App.Controller extends Spine.Controller
ws_send: (data) ->
App.Event.trigger( 'ws:send', JSON.stringify(data) )
# central method, is getting called on every ticket form change
ticketFormChanges: (params, attribute, attributes, classname, form, ui) =>
if @form_meta.dependencies && @form_meta.dependencies[attribute.name]
dependency = @form_meta.dependencies[attribute.name][ parseInt(params[attribute.name]) ]
if !dependency
dependency = @form_meta.dependencies[attribute.name][ params[attribute.name] ]
if dependency
for fieldNameToChange of dependency
filter = []
if dependency[fieldNameToChange]
filter = dependency[fieldNameToChange]
# find element to replace
for item in attributes
if item.name is fieldNameToChange
item['filter'] = {}
item['filter'][ fieldNameToChange ] = filter
item.default = params[item.name]
#if !item.default
# delete item['default']
newElement = ui.formGenItem( item, classname, form )
# replace new option list
form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith( newElement )
class App.ControllerPermanent extends App.Controller
constructor: ->
super
@ -485,10 +510,10 @@ class App.ControllerModal extends App.Controller
'.modal-body': 'body'
events:
'submit form': 'onSubmit'
'submit form': 'onSubmit'
'click .js-submit:not(.is-disabled)': 'onSubmit'
'click .js-cancel': 'hide'
'click .js-close': 'hide'
'click .js-cancel': 'hide'
'click .js-close': 'hide'
className: 'modal fade'

View file

@ -35,7 +35,11 @@ class App.ControllerForm extends App.Controller
formGen: ->
App.Log.debug 'ControllerForm', 'formGen', @model.configure_attributes
fieldset = $('<fieldset></fieldset>')
# check if own fieldset should be generated
if @noFieldset
fieldset = @el
else
fieldset = $('<fieldset></fieldset>')
# collect form attributes
@attributes = []
@ -81,9 +85,6 @@ class App.ControllerForm extends App.Controller
item = @formGenItem( attribute, className, fieldset, attribute_count )
item.appendTo(fieldset)
if @noFieldset
fieldset = fieldset.children()
if @fullForm
if !@formClass
@formClass = ''

View file

@ -205,29 +205,6 @@ class App.TicketCreate extends App.Controller
form_id: @form_id
)
formChanges = (params, attribute, attributes, classname, form, ui) =>
if @form_meta.dependencies && @form_meta.dependencies[attribute.name]
dependency = @form_meta.dependencies[attribute.name][ parseInt(params[attribute.name]) ]
if dependency
for fieldNameToChange of dependency
filter = []
if dependency[fieldNameToChange]
filter = dependency[fieldNameToChange]
# find element to replace
for item in attributes
if item.name is fieldNameToChange
item['filter'] = {}
item['filter'][ fieldNameToChange ] = filter
item.default = params[item.name]
#if !item.default
# delete item['default']
newElement = ui.formGenItem( item, classname, form )
# replace new option list
form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith( newElement )
signatureChanges = (params, attribute, attributes, classname, form, ui) =>
if attribute && attribute.name is 'group_id'
signature = undefined
@ -274,7 +251,7 @@ class App.TicketCreate extends App.Controller
events:
'change [name=customer_id]': @localUserInfo
handlers: [
formChanges,
@ticketFormChanges,
signatureChanges,
]
filter: @form_meta.filter
@ -297,7 +274,7 @@ class App.TicketCreate extends App.Controller
events:
'change [name=customer_id]': @localUserInfo
handlers: [
formChanges,
@ticketFormChanges,
signatureChanges,
]
filter: @form_meta.filter
@ -312,7 +289,7 @@ class App.TicketCreate extends App.Controller
events:
'change [name=customer_id]': @localUserInfo
handlers: [
formChanges,
@ticketFormChanges,
signatureChanges,
]
filter: @form_meta.filter

View file

@ -64,29 +64,6 @@ class Index extends App.ControllerContent
groupFilter = [groupFilter]
@form_meta.filter.group_id = groupFilter
formChanges = (params, attribute, attributes, classname, form, ui) =>
if @form_meta.dependencies && @form_meta.dependencies[attribute.name]
dependency = @form_meta.dependencies[attribute.name][ parseInt(params[attribute.name]) ]
if dependency
for fieldNameToChange of dependency
filter = []
if dependency[fieldNameToChange]
filter = dependency[fieldNameToChange]
# find element to replace
for item in attributes
if item.name is fieldNameToChange
item['filter'] = {}
item['filter'][ fieldNameToChange ] = filter
item.default = params[item.name]
#if !item.default
# delete item['default']
newElement = ui.formGenItem( item, classname, form )
# replace new option list
form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith( newElement )
@html App.view('customer_ticket_create')( head: 'New Ticket' )
new App.ControllerForm(
@ -95,7 +72,7 @@ class Index extends App.ControllerContent
model: App.Ticket
screen: 'create_top'
handlers: [
formChanges
@ticketFormChanges
]
filter: @form_meta.filter
autofocus: true
@ -115,7 +92,7 @@ class Index extends App.ControllerContent
model: App.Ticket
screen: 'create_middle'
handlers: [
formChanges
@ticketFormChanges
]
filter: @form_meta.filter
params: defaults

View file

@ -777,4 +777,16 @@ class insufficientRightsRef extends App.ControllerContent
App.Config.set( 'layout_ref/insufficient_rights', insufficientRightsRef, 'Routes' )
class errorRef extends App.ControllerContent
constructor: ->
super
@render()
render: ->
@html App.view('layout_ref/error')()
App.Config.set( 'layout_ref/error', errorRef, 'Routes' )
App.Config.set( 'LayoutRef', { prio: 1700, parent: '#current_user', name: 'Layout Reference', target: '#layout_ref', role: [ 'Admin' ] }, 'NavBarRight' )

View file

@ -115,6 +115,9 @@ class App.Navigation extends App.Controller
render: () ->
# reset result cache
@searchResultCache = {}
user = App.Session.get()
@html App.view('navigation')(
user: user
@ -127,6 +130,11 @@ class App.Navigation extends App.Controller
@renderPersonal()
searchFunction = =>
# use cache for search result
if @searchResultCache[@term]
@renderResult( @searchResultCache[@term] )
App.Ajax.request(
id: 'search'
type: 'GET'
@ -139,6 +147,9 @@ class App.Navigation extends App.Controller
# load assets
App.Collection.loadAssets( data.assets )
# cache search result
@searchResultCache[@term] = data.result
result = data.result
for area in result
if area.name is 'Ticket'
@ -222,7 +233,7 @@ class App.Navigation extends App.Controller
return if term is @term
@term = term
@$('.search').toggleClass('filled', !!@term)
@delay( searchFunction, 220, 'search' )
@delay( searchFunction, 200, 'search' )
)
# bind to empty search

View file

@ -267,29 +267,6 @@ class App.TicketZoom extends App.Controller
console.log('SHOW', ticket.id)
el.find('.edit').html('')
formChanges = (params, attribute, attributes, classname, form, ui) =>
if @form_meta.dependencies && @form_meta.dependencies[attribute.name]
dependency = @form_meta.dependencies[attribute.name][ parseInt(params[attribute.name]) ]
if dependency
for fieldNameToChange of dependency
filter = []
if dependency[fieldNameToChange]
filter = dependency[fieldNameToChange]
# find element to replace
for item in attributes
if item.name is fieldNameToChange
item['filter'] = {}
item['filter'][ fieldNameToChange ] = filter
item.default = params[item.name]
#if !item.default
# delete item['default']
newElement = ui.formGenItem( item, classname, form )
# replace new option list
form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith( newElement )
defaults = ticket.attributes()
task_state = @taskGet('ticket')
modelDiff = @getDiff( defaults, task_state )
@ -305,7 +282,7 @@ class App.TicketZoom extends App.Controller
screen: 'edit'
params: App.Ticket.find(ticket.id)
handlers: [
formChanges
@ticketFormChanges
]
filter: @form_meta.filter
params: defaults
@ -1502,10 +1479,10 @@ class ArticleView extends App.Controller
e.preventDefault()
# get reference article
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal( article_id )
type = App.TicketArticleType.find( article.type_id )
customer = App.User.find( article.created_by_id )
article_id = $(e.target).parents('[data-id]').data('id')
article = App.TicketArticle.fullLocal( article_id )
type = App.TicketArticleType.find( article.type_id )
customer = App.User.find( article.created_by_id )
@ui.el.find('.article-add').ScrollTo()
@ -1534,12 +1511,18 @@ class ArticleView extends App.Controller
to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
articleNew.to = to
else if type.name is 'email'
else if type.name is 'email' || type.name is 'phone' || type.name is 'web'
if article.sender.name is 'Agent'
articleNew.to = article.to
else
articleNew.to = article.from
# if sender is customer but in article.from is no email, try to get
# customers email via customer user
if articleNew.to && !articleNew.to.match(/@/)
articleNew.to = article.created_by.email
# filter for uniq recipients
recipientAddresses = {}
recipient = emailAddresses.parseAddressList(articleNew.to)
@ -1678,22 +1661,28 @@ class Article extends App.Controller
]
#if @article.type.name is 'note'
# actions.push []
if @article.type.name is 'email'
if @article.type.name is 'email' || @article.type.name is 'phone' || @article.type.name is 'web'
actions.push {
name: 'reply'
type: 'reply'
href: '#'
}
recipients = []
if @article.to
localRecipients = emailAddresses.parseAddressList(@article.to)
if localRecipients
recipients = recipients.concat localRecipients
if @article.sender.name is 'Agent'
if @article.to
localRecipients = emailAddresses.parseAddressList(@article.to)
if localRecipients
recipients = recipients.concat localRecipients
else
if @article.from
localRecipients = emailAddresses.parseAddressList(@article.from)
if localRecipients
recipients = recipients.concat localRecipients
if @article.cc
localRecipients = emailAddresses.parseAddressList(@article.cc)
if localRecipients
recipients = recipients.concat localRecipients
if recipients.length > 0
if recipients.length > 1
actions.push {
name: 'reply all'
type: 'replyAll'

View file

@ -0,0 +1,10 @@
class FFlt35
constructor: ->
data = App.Browser.detection()
if data.browser is 'Firefox' && data.version && data.version < 35
# for firefox lower 35 we need to set a class to hide own dropdown images
# whole file can be removed after dropping firefox 34 and lower support
$('html').addClass('ff-lt-35')
App.Config.set( 'aaa_ff-lt-35', FFlt35, 'Widgets' )

View file

@ -47,6 +47,7 @@ class App.OnlineNotificationWidget extends App.Controller
@toggle.append('<div class="activity-counter">' + count.toString() + '</div>')
markAllAsRead: =>
@counterUpdate(0)
@ajax(
id: 'markAllAsRead'
type: 'POST'
@ -63,14 +64,22 @@ class App.OnlineNotificationWidget extends App.Controller
onShow: =>
@updateContent()
# set heigth of notification popover
height = $('#app').height()
$('.js-notificationsContainer').css('height', "#{height-12}px")
$('.js-notificationsContainer .arrow').css('top', '20px')
# set height of notification popover
notificationsContainer = $('.js-notificationsContainer')
heightApp = $('#app').height()
heightPopoverSpacer = 36
heightPopoverHeader = notificationsContainer.find('.popover-notificationsHeader').outerHeight()
heightPopoverContent = notificationsContainer.find('.popover-content').get(0).scrollHeight
heightPopoverContentNew = heightPopoverContent
if (heightPopoverHeader + heightPopoverContent + heightPopoverSpacer) > heightApp
heightPopoverContentNew = heightApp - heightPopoverHeader - heightPopoverSpacer
notificationsContainer.addClass('is-overflowing')
else
notificationsContainer.removeClass('is-overflowing')
notificationsContainer.find('.popover-content').css('height', "#{heightPopoverContentNew}px")
# close notification list on click
$('.js-notificationsContainer').on('click', (e) =>
#console.log('CL')
@hidePopover()
)

View file

@ -1,828 +0,0 @@
/* Modernizr 2.8.3 (Custom Build) | MIT & BSD
* Build: http://modernizr.com/download/#-fontface-backgroundsize-borderimage-borderradius-boxshadow-flexbox-hsla-multiplebgs-opacity-rgba-textshadow-cssanimations-csscolumns-generatedcontent-cssgradients-cssreflections-csstransforms-csstransforms3d-csstransitions-applicationcache-canvas-canvastext-draganddrop-hashchange-history-audio-video-indexeddb-input-inputtypes-localstorage-postmessage-sessionstorage-websockets-websqldatabase-webworkers-geolocation-inlinesvg-smil-svg-svgclippaths-touch-webgl-shiv-cssclasses-addtest-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes-load
*/
;
window.Modernizr = (function( window, document, undefined ) {
var version = '2.8.3',
Modernizr = {},
enableClasses = true,
docElement = document.documentElement,
mod = 'modernizr',
modElem = document.createElement(mod),
mStyle = modElem.style,
inputElem = document.createElement('input') ,
smile = ':)',
toString = {}.toString,
prefixes = ' -webkit- -moz- -o- -ms- '.split(' '),
omPrefixes = 'Webkit Moz O ms',
cssomPrefixes = omPrefixes.split(' '),
domPrefixes = omPrefixes.toLowerCase().split(' '),
ns = {'svg': 'http://www.w3.org/2000/svg'},
tests = {},
inputs = {},
attrs = {},
classes = [],
slice = classes.slice,
featureName,
injectElementWithStyles = function( rule, callback, nodes, testnames ) {
var style, ret, node, docOverflow,
div = document.createElement('div'),
body = document.body,
fakeBody = body || document.createElement('body');
if ( parseInt(nodes, 10) ) {
while ( nodes-- ) {
node = document.createElement('div');
node.id = testnames ? testnames[nodes] : mod + (nodes + 1);
div.appendChild(node);
}
}
style = ['&#173;','<style id="s', mod, '">', rule, '</style>'].join('');
div.id = mod;
(body ? div : fakeBody).innerHTML += style;
fakeBody.appendChild(div);
if ( !body ) {
fakeBody.style.background = '';
fakeBody.style.overflow = 'hidden';
docOverflow = docElement.style.overflow;
docElement.style.overflow = 'hidden';
docElement.appendChild(fakeBody);
}
ret = callback(div, rule);
if ( !body ) {
fakeBody.parentNode.removeChild(fakeBody);
docElement.style.overflow = docOverflow;
} else {
div.parentNode.removeChild(div);
}
return !!ret;
},
isEventSupported = (function() {
var TAGNAMES = {
'select': 'input', 'change': 'input',
'submit': 'form', 'reset': 'form',
'error': 'img', 'load': 'img', 'abort': 'img'
};
function isEventSupported( eventName, element ) {
element = element || document.createElement(TAGNAMES[eventName] || 'div');
eventName = 'on' + eventName;
var isSupported = eventName in element;
if ( !isSupported ) {
if ( !element.setAttribute ) {
element = document.createElement('div');
}
if ( element.setAttribute && element.removeAttribute ) {
element.setAttribute(eventName, '');
isSupported = is(element[eventName], 'function');
if ( !is(element[eventName], 'undefined') ) {
element[eventName] = undefined;
}
element.removeAttribute(eventName);
}
}
element = null;
return isSupported;
}
return isEventSupported;
})(),
_hasOwnProperty = ({}).hasOwnProperty, hasOwnProp;
if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) {
hasOwnProp = function (object, property) {
return _hasOwnProperty.call(object, property);
};
}
else {
hasOwnProp = function (object, property) {
return ((property in object) && is(object.constructor.prototype[property], 'undefined'));
};
}
if (!Function.prototype.bind) {
Function.prototype.bind = function bind(that) {
var target = this;
if (typeof target != "function") {
throw new TypeError();
}
var args = slice.call(arguments, 1),
bound = function () {
if (this instanceof bound) {
var F = function(){};
F.prototype = target.prototype;
var self = new F();
var result = target.apply(
self,
args.concat(slice.call(arguments))
);
if (Object(result) === result) {
return result;
}
return self;
} else {
return target.apply(
that,
args.concat(slice.call(arguments))
);
}
};
return bound;
};
}
function setCss( str ) {
mStyle.cssText = str;
}
function setCssAll( str1, str2 ) {
return setCss(prefixes.join(str1 + ';') + ( str2 || '' ));
}
function is( obj, type ) {
return typeof obj === type;
}
function contains( str, substr ) {
return !!~('' + str).indexOf(substr);
}
function testProps( props, prefixed ) {
for ( var i in props ) {
var prop = props[i];
if ( !contains(prop, "-") && mStyle[prop] !== undefined ) {
return prefixed == 'pfx' ? prop : true;
}
}
return false;
}
function testDOMProps( props, obj, elem ) {
for ( var i in props ) {
var item = obj[props[i]];
if ( item !== undefined) {
if (elem === false) return props[i];
if (is(item, 'function')){
return item.bind(elem || obj);
}
return item;
}
}
return false;
}
function testPropsAll( prop, prefixed, elem ) {
var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1),
props = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' ');
if(is(prefixed, "string") || is(prefixed, "undefined")) {
return testProps(props, prefixed);
} else {
props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' ');
return testDOMProps(props, prefixed, elem);
}
} tests['flexbox'] = function() {
return testPropsAll('flexWrap');
}; tests['canvas'] = function() {
var elem = document.createElement('canvas');
return !!(elem.getContext && elem.getContext('2d'));
};
tests['canvastext'] = function() {
return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function'));
};
tests['webgl'] = function() {
return !!window.WebGLRenderingContext;
};
tests['touch'] = function() {
var bool;
if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
bool = true;
} else {
injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) {
bool = node.offsetTop === 9;
});
}
return bool;
};
tests['geolocation'] = function() {
return 'geolocation' in navigator;
};
tests['postmessage'] = function() {
return !!window.postMessage;
};
tests['websqldatabase'] = function() {
return !!window.openDatabase;
};
tests['indexedDB'] = function() {
return !!testPropsAll("indexedDB", window);
};
tests['hashchange'] = function() {
return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7);
};
tests['history'] = function() {
return !!(window.history && history.pushState);
};
tests['draganddrop'] = function() {
var div = document.createElement('div');
return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
};
tests['websockets'] = function() {
return 'WebSocket' in window || 'MozWebSocket' in window;
};
tests['rgba'] = function() {
setCss('background-color:rgba(150,255,150,.5)');
return contains(mStyle.backgroundColor, 'rgba');
};
tests['hsla'] = function() {
setCss('background-color:hsla(120,40%,100%,.5)');
return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla');
};
tests['multiplebgs'] = function() {
setCss('background:url(https://),url(https://),red url(https://)');
return (/(url\s*\(.*?){3}/).test(mStyle.background);
}; tests['backgroundsize'] = function() {
return testPropsAll('backgroundSize');
};
tests['borderimage'] = function() {
return testPropsAll('borderImage');
};
tests['borderradius'] = function() {
return testPropsAll('borderRadius');
};
tests['boxshadow'] = function() {
return testPropsAll('boxShadow');
};
tests['textshadow'] = function() {
return document.createElement('div').style.textShadow === '';
};
tests['opacity'] = function() {
setCssAll('opacity:.55');
return (/^0.55$/).test(mStyle.opacity);
};
tests['cssanimations'] = function() {
return testPropsAll('animationName');
};
tests['csscolumns'] = function() {
return testPropsAll('columnCount');
};
tests['cssgradients'] = function() {
var str1 = 'background-image:',
str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));',
str3 = 'linear-gradient(left top,#9f9, white);';
setCss(
(str1 + '-webkit- '.split(' ').join(str2 + str1) +
prefixes.join(str3 + str1)).slice(0, -str1.length)
);
return contains(mStyle.backgroundImage, 'gradient');
};
tests['cssreflections'] = function() {
return testPropsAll('boxReflect');
};
tests['csstransforms'] = function() {
return !!testPropsAll('transform');
};
tests['csstransforms3d'] = function() {
var ret = !!testPropsAll('perspective');
if ( ret && 'webkitPerspective' in docElement.style ) {
injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) {
ret = node.offsetLeft === 9 && node.offsetHeight === 3;
});
}
return ret;
};
tests['csstransitions'] = function() {
return testPropsAll('transition');
};
tests['fontface'] = function() {
var bool;
injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) {
var style = document.getElementById('smodernizr'),
sheet = style.sheet || style.styleSheet,
cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : '';
bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0;
});
return bool;
};
tests['generatedcontent'] = function() {
var bool;
injectElementWithStyles(['#',mod,'{font:0/0 a}#',mod,':after{content:"',smile,'";visibility:hidden;font:3px/1 a}'].join(''), function( node ) {
bool = node.offsetHeight >= 3;
});
return bool;
};
tests['video'] = function() {
var elem = document.createElement('video'),
bool = false;
try {
if ( bool = !!elem.canPlayType ) {
bool = new Boolean(bool);
bool.ogg = elem.canPlayType('video/ogg; codecs="theora"') .replace(/^no$/,'');
bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"') .replace(/^no$/,'');
bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,'');
}
} catch(e) { }
return bool;
};
tests['audio'] = function() {
var elem = document.createElement('audio'),
bool = false;
try {
if ( bool = !!elem.canPlayType ) {
bool = new Boolean(bool);
bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,'');
bool.mp3 = elem.canPlayType('audio/mpeg;') .replace(/^no$/,'');
bool.wav = elem.canPlayType('audio/wav; codecs="1"') .replace(/^no$/,'');
bool.m4a = ( elem.canPlayType('audio/x-m4a;') ||
elem.canPlayType('audio/aac;')) .replace(/^no$/,'');
}
} catch(e) { }
return bool;
};
tests['localstorage'] = function() {
try {
localStorage.setItem(mod, mod);
localStorage.removeItem(mod);
return true;
} catch(e) {
return false;
}
};
tests['sessionstorage'] = function() {
try {
sessionStorage.setItem(mod, mod);
sessionStorage.removeItem(mod);
return true;
} catch(e) {
return false;
}
};
tests['webworkers'] = function() {
return !!window.Worker;
};
tests['applicationcache'] = function() {
return !!window.applicationCache;
};
tests['svg'] = function() {
return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect;
};
tests['inlinesvg'] = function() {
var div = document.createElement('div');
div.innerHTML = '<svg/>';
return (div.firstChild && div.firstChild.namespaceURI) == ns.svg;
};
tests['smil'] = function() {
return !!document.createElementNS && /SVGAnimate/.test(toString.call(document.createElementNS(ns.svg, 'animate')));
};
tests['svgclippaths'] = function() {
return !!document.createElementNS && /SVGClipPath/.test(toString.call(document.createElementNS(ns.svg, 'clipPath')));
};
function webforms() {
Modernizr['input'] = (function( props ) {
for ( var i = 0, len = props.length; i < len; i++ ) {
attrs[ props[i] ] = !!(props[i] in inputElem);
}
if (attrs.list){
attrs.list = !!(document.createElement('datalist') && window.HTMLDataListElement);
}
return attrs;
})('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' '));
Modernizr['inputtypes'] = (function(props) {
for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) {
inputElem.setAttribute('type', inputElemType = props[i]);
bool = inputElem.type !== 'text';
if ( bool ) {
inputElem.value = smile;
inputElem.style.cssText = 'position:absolute;visibility:hidden;';
if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) {
docElement.appendChild(inputElem);
defaultView = document.defaultView;
bool = defaultView.getComputedStyle &&
defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' &&
(inputElem.offsetHeight !== 0);
docElement.removeChild(inputElem);
} else if ( /^(search|tel)$/.test(inputElemType) ){
} else if ( /^(url|email)$/.test(inputElemType) ) {
bool = inputElem.checkValidity && inputElem.checkValidity() === false;
} else {
bool = inputElem.value != smile;
}
}
inputs[ props[i] ] = !!bool;
}
return inputs;
})('search tel url email datetime date month week time datetime-local number range color'.split(' '));
}
for ( var feature in tests ) {
if ( hasOwnProp(tests, feature) ) {
featureName = feature.toLowerCase();
Modernizr[featureName] = tests[feature]();
classes.push((Modernizr[featureName] ? '' : 'no-') + featureName);
}
}
Modernizr.input || webforms();
Modernizr.addTest = function ( feature, test ) {
if ( typeof feature == 'object' ) {
for ( var key in feature ) {
if ( hasOwnProp( feature, key ) ) {
Modernizr.addTest( key, feature[ key ] );
}
}
} else {
feature = feature.toLowerCase();
if ( Modernizr[feature] !== undefined ) {
return Modernizr;
}
test = typeof test == 'function' ? test() : test;
if (typeof enableClasses !== "undefined" && enableClasses) {
docElement.className += ' ' + (test ? '' : 'no-') + feature;
}
Modernizr[feature] = test;
}
return Modernizr;
};
setCss('');
modElem = inputElem = null;
;(function(window, document) {
var version = '3.7.0';
var options = window.html5 || {};
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
var supportsHtml5Styles;
var expando = '_html5shiv';
var expanID = 0;
var expandoData = {};
var supportsUnknownElements;
(function() {
try {
var a = document.createElement('a');
a.innerHTML = '<xyz></xyz>';
supportsHtml5Styles = ('hidden' in a);
supportsUnknownElements = a.childNodes.length == 1 || (function() {
(document.createElement)('a');
var frag = document.createDocumentFragment();
return (
typeof frag.cloneNode == 'undefined' ||
typeof frag.createDocumentFragment == 'undefined' ||
typeof frag.createElement == 'undefined'
);
}());
} catch(e) {
supportsHtml5Styles = true;
supportsUnknownElements = true;
}
}());
function addStyleSheet(ownerDocument, cssText) {
var p = ownerDocument.createElement('p'),
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
p.innerHTML = 'x<style>' + cssText + '</style>';
return parent.insertBefore(p.lastChild, parent.firstChild);
}
function getElements() {
var elements = html5.elements;
return typeof elements == 'string' ? elements.split(' ') : elements;
}
function getExpandoData(ownerDocument) {
var data = expandoData[ownerDocument[expando]];
if (!data) {
data = {};
expanID++;
ownerDocument[expando] = expanID;
expandoData[expanID] = data;
}
return data;
}
function createElement(nodeName, ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createElement(nodeName);
}
if (!data) {
data = getExpandoData(ownerDocument);
}
var node;
if (data.cache[nodeName]) {
node = data.cache[nodeName].cloneNode();
} else if (saveClones.test(nodeName)) {
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
} else {
node = data.createElem(nodeName);
}
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
}
function createDocumentFragment(ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createDocumentFragment();
}
data = data || getExpandoData(ownerDocument);
var clone = data.frag.cloneNode(),
i = 0,
elems = getElements(),
l = elems.length;
for(;i<l;i++){
clone.createElement(elems[i]);
}
return clone;
}
function shivMethods(ownerDocument, data) {
if (!data.cache) {
data.cache = {};
data.createElem = ownerDocument.createElement;
data.createFrag = ownerDocument.createDocumentFragment;
data.frag = data.createFrag();
}
ownerDocument.createElement = function(nodeName) {
if (!html5.shivMethods) {
return data.createElem(nodeName);
}
return createElement(nodeName, ownerDocument, data);
};
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
'var n=f.cloneNode(),c=n.createElement;' +
'h.shivMethods&&(' +
getElements().join().replace(/[\w\-]+/g, function(nodeName) {
data.createElem(nodeName);
data.frag.createElement(nodeName);
return 'c("' + nodeName + '")';
}) +
');return n}'
)(html5, data.frag);
}
function shivDocument(ownerDocument) {
if (!ownerDocument) {
ownerDocument = document;
}
var data = getExpandoData(ownerDocument);
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
data.hasCSS = !!addStyleSheet(ownerDocument,
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
'mark{background:#FF0;color:#000}' +
'template{display:none}'
);
}
if (!supportsUnknownElements) {
shivMethods(ownerDocument, data);
}
return ownerDocument;
}
var html5 = {
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video',
'version': version,
'shivCSS': (options.shivCSS !== false),
'supportsUnknownElements': supportsUnknownElements,
'shivMethods': (options.shivMethods !== false),
'type': 'default',
'shivDocument': shivDocument,
createElement: createElement,
createDocumentFragment: createDocumentFragment
};
window.html5 = html5;
shivDocument(document);
}(this, document));
Modernizr._version = version;
Modernizr._prefixes = prefixes;
Modernizr._domPrefixes = domPrefixes;
Modernizr._cssomPrefixes = cssomPrefixes;
Modernizr.hasEvent = isEventSupported;
Modernizr.testProp = function(prop){
return testProps([prop]);
};
Modernizr.testAllProps = testPropsAll;
Modernizr.testStyles = injectElementWithStyles;
Modernizr.prefixed = function(prop, obj, elem){
if(!obj) {
return testPropsAll(prop, 'pfx');
} else {
return testPropsAll(prop, obj, elem);
}
};
docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') +
(enableClasses ? ' js ' + classes.join(' ') : '');
return Modernizr;
})(this, this.document);
/*yepnope1.5.4|WTFPL*/
(function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f<d;f++)g=a[f].split("="),(e=z[g.shift()])&&(c=e(c,g));for(f=0;f<b;f++)c=x[f](c);return c}function g(a,e,f,g,h){var i=b(a),j=i.autoCallback;i.url.split(".").pop().split("?").shift(),i.bypass||(e&&(e=d(e)?e:e[a]||e[g]||e[a.split("/").pop().split("?")[0]]),i.instead?i.instead(a,e,f,g,h):(y[i.url]?i.noexec=!0:y[i.url]=1,f.load(i.url,i.forceCSS||!i.forceJS&&"css"==i.url.split(".").pop().split("?").shift()?"c":c,i.noexec,i.attrs,i.timeout),(d(e)||d(j))&&f.load(function(){k(),e&&e(i.origUrl,h,g),j&&j(i.origUrl,h,g),y[i.url]=2})))}function h(a,b){function c(a,c){if(a){if(e(a))c||(j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}),g(a,j,b,0,h);else if(Object(a)===a)for(n in m=function(){var b=0,c;for(c in a)a.hasOwnProperty(c)&&b++;return b}(),a)a.hasOwnProperty(n)&&(!c&&!--m&&(d(j)?j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}:j[n]=function(a){return function(){var b=[].slice.call(arguments);a&&a.apply(this,b),l()}}(k[n])),g(a[n],j,b,n,h))}else!c&&l()}var h=!!a.test,i=a.load||a.both,j=a.callback||f,k=j,l=a.complete||f,m,n;c(h?a.yep:a.nope,!!i),i&&c(i)}var i,j,l=this.yepnope.loader;if(e(a))g(a,0,l,0);else if(w(a))for(i=0;i<a.length;i++)j=a[i],e(j)?g(j,0,l,0):w(j)?B(j):Object(j)===j&&h(j,l);else Object(a)===a&&h(a,l)},B.addPrefix=function(a,b){z[a]=b},B.addFilter=function(a){x.push(a)},B.errorTimeout=1e4,null==b.readyState&&b.addEventListener&&(b.readyState="loading",b.addEventListener("DOMContentLoaded",A=function(){b.removeEventListener("DOMContentLoaded",A,0),b.readyState="complete"},0)),a.yepnope=k(),a.yepnope.executeStack=h,a.yepnope.injectJs=function(a,c,d,e,i,j){var k=b.createElement("script"),l,o,e=e||B.errorTimeout;k.src=a;for(o in d)k.setAttribute(o,d[o]);c=j?h:c||f,k.onreadystatechange=k.onload=function(){!l&&g(k.readyState)&&(l=1,c(),k.onload=k.onreadystatechange=null)},m(function(){l||(l=1,c(1))},e),i?k.onload():n.parentNode.insertBefore(k,n)},a.yepnope.injectCss=function(a,c,d,e,g,i){var e=b.createElement("link"),j,c=i?h:c||f;e.href=a,e.rel="stylesheet",e.type="text/css";for(j in d)e.setAttribute(j,d[j]);g||(n.parentNode.insertBefore(e,n),m(c,0))}})(this,document);
Modernizr.load=function(){yepnope.apply(window,[].slice.call(arguments,0));};
;

View file

@ -0,0 +1,4 @@
/* Modernizr 2.8.3 (Custom Build) | MIT & BSD
* Build: http://modernizr.com/download/#-prefixed-testprop-testallprops-domprefixes
*/
;window.Modernizr=function(a,b,c){function w(a){i.cssText=a}function x(a,b){return w(prefixes.join(a+";")+(b||""))}function y(a,b){return typeof a===b}function z(a,b){return!!~(""+a).indexOf(b)}function A(a,b){for(var d in a){var e=a[d];if(!z(e,"-")&&i[e]!==c)return b=="pfx"?e:!0}return!1}function B(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:y(f,"function")?f.bind(d||b):f}return!1}function C(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+m.join(d+" ")+d).split(" ");return y(b,"string")||y(b,"undefined")?A(e,b):(e=(a+" "+n.join(d+" ")+d).split(" "),B(e,b,c))}var d="2.8.3",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l="Webkit Moz O ms",m=l.split(" "),n=l.toLowerCase().split(" "),o={},p={},q={},r=[],s=r.slice,t,u={}.hasOwnProperty,v;!y(u,"undefined")&&!y(u.call,"undefined")?v=function(a,b){return u.call(a,b)}:v=function(a,b){return b in a&&y(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=s.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(s.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(s.call(arguments)))};return e});for(var D in o)v(o,D)&&(t=D.toLowerCase(),e[t]=o[D](),r.push((e[t]?"":"no-")+t));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)v(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},w(""),h=j=null,e._version=d,e._domPrefixes=n,e._cssomPrefixes=m,e.testProp=function(a){return A([a])},e.testAllProps=C,e.prefixed=function(a,b,c){return b?C(a,b,c):C(a,"pfx")},e}(this,this.document);

View file

@ -1,5 +1,3 @@
<div class="flex fullscreenMessage">
<div class="error icon"></div><h2><%- @T('Opps.. I\'m sorry, something went wrong!' ) %></h2>
<p><%- @T('Status Code') %>: <%= @status %></p>
<p><%= @detail %></p>
<div class="error icon"></div><h2><%- @T('Status Code') %>: <%= @status %>. <%= @detail %></h2>
</div>

View file

@ -0,0 +1,3 @@
<div class="flex fullscreenMessage">
<div class="error icon"></div><h2><%- @T('Status Code') %>: 1234. may be not internet connection...</h2>
</div>

View file

@ -22,6 +22,7 @@
<li><a href="#layout_ref/local_modal">Local Modal</a></li>
<li><a href="#layout_ref/loading_placeholder">Loading Placeholder</a></li>
<li><a href="#layout_ref/insufficient_rights">Insufficient Rights Warning</a></li>
<li><a href="#layout_ref/error">Error</a></li>
</ul>
</div>

View file

@ -1,7 +1,7 @@
<% for item in @item_list: %>
<a href="<%- item.data.url %>" title="<%= item.data.title %>" class="task <%= item.data.class %> horizontal center <% if item.task.active: %>active<% end %>" data-key="<%- item.task.key %>">
<a href="<%- item.data.url %>" title="<%= item.data.title %>" class="task <%= item.data.class %> <% if item.task.active: %>active<% end %>" data-key="<%- item.task.key %>">
<div class="icon-holder centered">
<div class="<%= item.data.iconClass %> icon<% if item.task.notify: %> modified<% end %>"></div>
<div class="<% if item.task.active: %>white <% end %><%= item.data.iconClass %> icon<% if item.task.notify: %> modified<% end %>"></div>
</div>
<div class="name u-textTruncate flex"><%= item.data.head %></div>
<div class="closeTask js-close u-clickable horizontal centered" title="<%- @T('close') %>">

View file

@ -86,7 +86,7 @@
<% if article.actions: %>
<div>
<div class="article-actions horizontal stretch">
<div class="article-content article-actions horizontal stretch">
<% for action in article.actions: %>
<a href="<%= action.href %>" data-type="<%= action.type %>" class="article-action u-clickable<% if action.class: %> <%= action.class %><% end %>">
<span class="<%= action.type %> action icon"></span><%- @T( action.name ) %>

View file

@ -284,6 +284,7 @@ span[data-tooltip]:hover:before {
&.btn--action {
@extend label;
height: 31px;
padding: 7px 11px 5px !important;
}
@ -732,16 +733,17 @@ textarea,
Firefox only hack
-----------------
Firefox doesn't allow us to hide the dropdown arrow
but we want to replace it with our own icon.
So we have to hide our own icon in Firefox.
Firefox below version 35 doesn't allow us to
hide the dropdown arrow but we want to replace
it with our own icon. So we have to hide our own
icon in Firefox versions under 35.
The class is set via Javascript
*/
@-moz-document url-prefix() {
.form-control + .select-arrow {
display: none;
}
html.ff-lt-35 .form-control + .select-arrow {
display: none;
}
select::-ms-expand {
@ -843,7 +845,6 @@ textarea,
margin-top: 6px;
margin-left: auto;
@extend .horizontal;
@extend .justify;
@extend .self-start;
.btn {
@ -1336,17 +1337,19 @@ ol.tabs li {
height: 6px;
border-radius: 100%;
background: #2c2d36;
will-change: opacity;
transform: translateZ(0);
}
.modified.priority.icon:after {
-webkit-animation: fade 1s ease 2s infinite alternate;
-moz-animation: fade 1s ease 2s infinite alternate;
animation: fade 1s ease 2s infinite alternate;
-webkit-animation: fade 2s ease-in-out infinite;
-moz-animation: fade 2s ease-in-out infinite;
animation: fade 2s ease-in-out infinite;
}
@-webkit-keyframes fade { from { opacity: 0 } to { opacity: 1 } }
@-moz-keyframes fade { from { opacity: 0 } to { opacity: 1 } }
@keyframes fade { from { opacity: 0 } to { opacity: 1 } }
@-webkit-keyframes fade { from { opacity: 1 } 50% { opacity: 0 } to { opacity: 1 } }
@-moz-keyframes fade { from { opacity: 1 } 50% { opacity: 0 } to { opacity: 1 } }
@keyframes fade { from { opacity: 1 } 50% { opacity: 0 } to { opacity: 1 } }
.organization.icon {
height: 13px;
@ -1355,7 +1358,6 @@ ol.tabs li {
}
.icon-switch:hover .organization.icon,
.task.active .organization.icon,
.white.organization.icon {
background-position: 0 -132px;
}
@ -1367,7 +1369,6 @@ ol.tabs li {
}
.icon-switch:hover .user.icon,
.task.active .user.icon,
.white.user.icon {
background-position: -15px -132px;
}
@ -1379,7 +1380,6 @@ ol.tabs li {
}
.icon-switch:hover .note.icon,
.task.active .note.icon,
.white.note.icon {
background-position: -30px -132px;
}
@ -1391,7 +1391,6 @@ ol.tabs li {
}
.icon-switch:hover .pen.icon,
.task.active .pen.icon,
.white.pen.icon {
background-position: -45px -132px;
}
@ -1402,7 +1401,6 @@ ol.tabs li {
background-position: -60px -118px;
}
.icon-switch:hover .important.icon,
.task.active .important.icon,
.white.important.icon {
background-position: -60px -132px;
}
@ -1413,7 +1411,6 @@ ol.tabs li {
background-position: -75px -118px;
}
.icon-switch:hover .tools.icon,
.task.active .tools.icon,
.white.tools.icon {
background-position: -75px -132px;
}
@ -1424,7 +1421,6 @@ ol.tabs li {
background-position: -90px -118px;
}
.icon-switch:hover .clock.icon,
.task.active .clock.icon,
.white.clock.icon {
background-position: -90px -132px;
}
@ -1432,10 +1428,37 @@ ol.tabs li {
.team.icon {
height: 13px;
width: 20px;
background-position: -105px -118px;
background-position: -104px -118px;
}
.white.team.icon {
background-position: -105px -132px;
background-position: -104px -132px;
}
.cloud.icon {
height: 12px;
width: 15px;
background-position: -125px -119px;
}
.white.cloud.icon {
background-position: -125px -133px;
}
.package.icon {
height: 16px;
width: 15px;
background-position: -141px -112px;
}
.white.package.icon {
background-position: -141px -129px;
}
.list.icon {
height: 14px;
width: 15px;
background-position: -157px -116px;
}
.white.list.icon {
background-position: -157px -131px;
}
.channel.icon {
@ -2074,6 +2097,8 @@ footer {
padding: 10px 15px 7px 0;
position: relative;
@extend .u-clickable;
@extend .horizontal;
@extend .center;
}
.tasks-navigation .task {
@ -2254,6 +2279,7 @@ footer {
.search.focused .logo {
opacity: 0;
z-index: -1;
}
.search .logo {
@ -2707,12 +2733,17 @@ footer {
}
.popover--notifications {
min-height: 100px;
@extend .zIndex-5;
&.is-visible {
@extend .vertical;
}
.arrow {
top: 23px !important;
}
.popover-content {
@extend .flex;
}
@ -4160,11 +4191,14 @@ footer {
display: block;
padding-left: 40px;
position: absolute;
overflow: auto;
background: hsla(210,17%,98%,.55);
bottom: auto;
min-height: 100vh;
.modal-backdrop {
display: none;
position: absolute;
background: hsla(210,17%,93%,.55);
height: 100% !important;
opacity: 1;
}
.modal-dialog {

View file

@ -279,8 +279,10 @@ class TicketsController < ApplicationController
:assets => assets,
:links => link_list,
:tags => tags,
:form_meta => attributes_to_change,
:edit_form => attributes_to_change,
:form_meta => {
:filter => attributes_to_change[:filter],
:dependencies => attributes_to_change[:dependencies],
}
}
end

View file

@ -69,7 +69,7 @@ class Observer::Ticket::Notification::BackgroundJob
:object => 'Ticket',
:o_id => ticket.id,
:seen => false,
:created_by_id => ticket.created_by_id || 1,
:created_by_id => ticket.updated_by_id || 1,
:user_id => user.id,
)
@ -292,7 +292,7 @@ State: i18n(#{ticket.state.name.text2html})<br>
<br>'
end
if user.preferences[:locale] =~ /^de/i
subject = 'Ticket aktualisiert (#{ticket.title.text2html})'
subject = 'Ticket aktualisiert (#{ticket.title})'
body = '<div>Hallo #{recipient.firstname.text2html},</div>
<br>
<div>
@ -309,7 +309,7 @@ Ticket (#{ticket.title.text2html}) wurde von "<b>#{ticket.updated_by.fullname.te
</div>
'
else
subject = 'Updated Ticket (#{ticket.title.text2html})'
subject = 'Updated Ticket (#{ticket.title})'
body = '<div>Hi #{recipient.firstname.text2html},</div>
<br>
<div>

View file

@ -32,12 +32,12 @@ list attributes
returns
result = {
:type_id => type_ids,
:state_id => state_ids,
:priority_id => priority_ids,
:owner_id => owner_ids,
:group_id => group_ids,
:group_id__owner_id => groups_users,
:type_id => type_ids,
:state_id => state_ids,
:priority_id => priority_ids,
:owner_id => owner_ids,
:group_id => group_ids,
:group_id__owner_id => groups_users,
}
=end
@ -103,7 +103,7 @@ returns
agents[ user.id ] = 1
}
dependencies = { :group_id => { '' => [] } }
dependencies = { :group_id => { '' => { :owner_id => [] } } }
Group.where( :active => true ).each { |group|
assets = group.assets(assets)
dependencies[:group_id][group.id] = { :owner_id => [] }
@ -115,9 +115,9 @@ returns
}
return {
:assets => assets,
:filter => filter,
:dependencies => dependencies,
:assets => assets,
:filter => filter,
:dependencies => dependencies,
}
end
@ -126,8 +126,8 @@ returns
list tickets by customer groupd in state categroie open and closed
result = Ticket::ScreenOptions.list_by_customer(
:customer_id => 123,
:limit => 15, # optional, default 15
:customer_id => 123,
:limit => 15, # optional, default 15
)
returns
@ -147,13 +147,13 @@ returns
# get tickets
tickets_open = Ticket.where(
:customer_id => data[:customer_id],
:state_id => state_list_open
:customer_id => data[:customer_id],
:state_id => state_list_open
).limit( data[:limit] || 15 ).order('created_at DESC')
tickets_closed = Ticket.where(
:customer_id => data[:customer_id],
:state_id => state_list_closed
:customer_id => data[:customer_id],
:state_id => state_list_closed
).limit( data[:limit] || 15 ).order('created_at DESC')
return {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View file

@ -0,0 +1,362 @@
# encoding: utf-8
require 'browser_test_helper'
class AgentTicketActionLevel5Test < TestCase
def test_agent_signature_check
suffix = rand(99999999999999999).to_s
signature_name1 = 'sig name 1 äöüß ' + suffix
signature_body1 = "--\nsig body 1 äöüß " + suffix
signature_name2 = 'sig name 2 äöüß ' + suffix
signature_body2 = "--\nsig body 2 äöüß " + suffix
group_name1 = "group name 1 " + suffix
group_name2 = "group name 2 " + suffix
group_name3 = "group name 3 " + suffix
tests = [
{
:name => 'create groups and signatures',
:action => [
{
:execute => 'close_all_tasks',
},
# create signatures
{
:execute => 'create_signature',
:name => signature_name1,
:body => signature_body1,
},
{
:execute => 'create_signature',
:name => signature_name2,
:body => signature_body2,
},
# create groups
{
:execute => 'create_group',
:name => group_name1,
:signature => signature_name1,
:member => [
'master@example.com'
],
},
{
:execute => 'create_group',
:name => group_name2,
:signature => signature_name2,
:member => [
'master@example.com'
],
},
{
:execute => 'create_group',
:name => group_name3,
:member => [
'master@example.com'
],
},
],
},
{
:name => 'check signature in new ticket',
:action => [
# reload instances to get new group permissions
{
:execute => 'reload',
},
{
:execute => 'create_ticket',
:group => 'Users',
:subject => 'some subject 4 - 123äöü',
:body => 'some body 4 - 123äöü',
:do_not_submit => true,
},
# check content
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => 'some body 4',
:no_quote => true,
:match_result => true,
},
# select group
{
:execute => 'select',
:css => '.active [name="group_id"]',
:value => group_name1,
},
# select group
{
:execute => 'select',
:css => '.active [name="group_id"]',
:value => group_name1,
},
# check content
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => 'some body 4',
:no_quote => true,
:match_result => true,
},
# check signature
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => false,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => false,
},
# select create channel
{
:execute => 'click',
:css => '.active [data-type="email-out"]',
},
# select group
{
:execute => 'select',
:css => '.active select[name="group_id"]',
:value => group_name1,
},
# check content
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => 'some body 4',
:no_quote => true,
:match_result => true,
},
# check signature
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => true,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => false,
},
# select group
{
:execute => 'select',
:css => '.active select[name="group_id"]',
:value => group_name2,
},
# check content
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => 'some body 4',
:no_quote => true,
:match_result => true,
},
# check signature
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => false,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => true,
},
# select group
{
:execute => 'select',
:css => '.active select[name="group_id"]',
:value => group_name3,
},
# check content
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => 'some body 4',
:no_quote => true,
:match_result => true,
},
# check signature
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => false,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => false,
},
# select group
{
:execute => 'select',
:css => '.active select[name="group_id"]',
:value => group_name1,
},
# check content
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => 'some body 4',
:no_quote => true,
:match_result => true,
},
# check signature
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => true,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => false,
},
# select create channel
{
:execute => 'click',
:css => '.active [data-type="phone-out"]',
},
# check content
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => 'some body 4',
:no_quote => true,
:match_result => true,
},
# check signature
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => false,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => false,
},
],
},
{
:name => 'check signature in zoom ticket',
:action => [
{
:execute => 'create_ticket',
:group => group_name1,
:subject => 'some subject 5 - 123äöü',
:body => 'some body 5 - 123äöü',
},
{
:execute => 'wait',
:value => 3,
},
# execute reply
{
:execute => 'click',
:css => '.active [data-type="reply"]',
},
# check if signature exists
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => true,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => false,
},
# discard changes
{
:execute => 'click',
:css => '.active .js-reset',
},
{
:execute => 'wait',
:value => 3,
},
# check if signature exists
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body1,
:no_quote => true,
:match_result => false,
},
{
:execute => 'match',
:css => '.active [data-name="body"]',
:value => signature_body2,
:no_quote => true,
:match_result => false,
},
],
},
]
browser_signle_test_with_login(tests, { :username => 'master@example.com' })
end
end

View file

@ -30,8 +30,12 @@ class TestCase < Test::Unit::TestCase
end
caps = Selenium::WebDriver::Remote::Capabilities.send( browser )
caps.platform = ENV['BROWSER_OS'] || 'Windows 2008'
caps.version = ENV['BROWSER_VERSION'] || '8'
if ENV['BROWSER_OS']
caps.platform = ENV['BROWSER_OS']
end
if ENV['BROWSER_VERSION']
caps.version = ENV['BROWSER_VERSION']
end
local_browser = Selenium::WebDriver.for(
:remote,
:url => ENV['REMOTE_URL'],
@ -257,18 +261,22 @@ class TestCase < Test::Unit::TestCase
if action[:timeout]
timeout = action[:timeout]
end
loops = (timeout / 2).to_i
loops = (timeout).to_i
text = ''
(1..loops).each { |loop|
element = instance.find_elements( { :css => action[:area] } )[0]
if element #&& element.displayed?
text = element.text
if text =~ /#{action[:value]}/i
assert( true, "(#{test[:name]}) '#{action[:value]}' found in '#{text}'" )
return
begin
text = element.text
if text =~ /#{action[:value]}/i
assert( true, "(#{test[:name]}) '#{action[:value]}' found in '#{text}'" )
return
end
rescue
# just try again
end
end
sleep 2
sleep 1
}
assert( false, "(#{test[:name]}) '#{action[:value]}' found in '#{text}'" )
return
@ -291,7 +299,6 @@ class TestCase < Test::Unit::TestCase
assert( false, "(#{test[:name]} / #{test[:area]}) still exsists" )
return
elsif action[:execute] == 'create_user'
instance.find_elements( { :css => 'a[href="#manage"]' } )[0].click
instance.find_elements( { :css => 'a[href="#manage/users"]' } )[0].click
sleep 2
@ -329,6 +336,82 @@ class TestCase < Test::Unit::TestCase
assert( true, "(#{test[:name]}) user creation failed" )
return
elsif action[:execute] == 'create_signature'
instance.find_elements( { :css => 'a[href="#manage"]' } )[0].click
instance.find_elements( { :css => 'a[href="#channels/email"]' } )[0].click
instance.find_elements( { :css => 'a[href="#c-signature"]' } )[0].click
sleep 8
instance.find_elements( { :css => '#content #c-signature a[data-type="new"]' } )[0].click
sleep 2
element = instance.find_elements( { :css => '.modal input[name=name]' } )[0]
element.clear
element.send_keys( action[:name] )
element = instance.find_elements( { :css => '.modal textarea[name=body]' } )[0]
element.clear
element.send_keys( action[:body] )
instance.find_elements( { :css => '.modal button.js-submit' } )[0].click
(1..12).each {|loop|
element = instance.find_elements( { :css => 'body' } )[0]
text = element.text
if text =~ /#{Regexp.quote(action[:name])}/
assert( true, "(#{test[:name]}) signature created" )
return
end
sleep 1
}
assert( true, "(#{test[:name]}) signature creation failed" )
return
elsif action[:execute] == 'create_group'
instance.find_elements( { :css => 'a[href="#manage"]' } )[0].click
instance.find_elements( { :css => 'a[href="#manage/groups"]' } )[0].click
sleep 2
instance.find_elements( { :css => 'a[data-type="new"]' } )[0].click
sleep 2
element = instance.find_elements( { :css => '.modal input[name=name]' } )[0]
element.clear
element.send_keys( action[:name] )
element = instance.find_elements( { :css => '.modal select[name="email_address_id"]' } )[0]
dropdown = Selenium::WebDriver::Support::Select.new(element)
dropdown.select_by( :index, 1 )
#dropdown.select_by( :text, action[:group])
if action[:signature]
element = instance.find_elements( { :css => '.modal select[name="signature_id"]' } )[0]
dropdown = Selenium::WebDriver::Support::Select.new(element)
dropdown.select_by( :text, action[:signature])
end
instance.find_elements( { :css => '.modal button.js-submit' } )[0].click
(1..12).each {|loop|
element = instance.find_elements( { :css => 'body' } )[0]
text = element.text
if text =~ /#{Regexp.quote(action[:name])}/
assert( true, "(#{test[:name]}) group created" )
# add member
if action[:member]
action[:member].each {|login|
instance.find_elements( { :css => 'a[href="#manage"]' } )[0].click
instance.find_elements( { :css => 'a[href="#manage/users"]' } )[0].click
sleep 2
element = instance.find_elements( { :css => '#content [name="search"]' } )[0]
element.clear
element.send_keys( login )
sleep 2
#instance.find_elements( { :css => '#content table [data-id]' } )[0].click
instance.execute_script( '$("#content table [data-id] td").first().click()' )
sleep 2
#instance.find_elements( { :css => 'label:contains(" ' + action[:name] + '")' } )[0].click
instance.execute_script( '$(\'label:contains(" ' + action[:name] + '")\').first().click()' )
instance.find_elements( { :css => '.modal button.js-submit' } )[0].click
}
end
return
end
sleep 1
}
assert( true, "(#{test[:name]}) group creation failed" )
return
elsif action[:execute] == 'verify_task_attributes'
if action[:title]
text = instance.find_elements( { :css => '.tasks .active' } )[0].text.strip
@ -389,6 +472,11 @@ class TestCase < Test::Unit::TestCase
return
end
sleep 2
# check count of agents, should be only 1 / - selection on init screen
count = instance.find_elements( { :css => '.active .newTicket select[name="owner_id"] option' } ).count
assert_equal( 1, count, 'check if owner selection is empty per default' )
if action[:group]
element = instance.find_elements( { :css => '.active .newTicket select[name="group_id"]' } )[0]
dropdown = Selenium::WebDriver::Support::Select.new(element)

View file

@ -0,0 +1,142 @@
# encoding: utf-8
require 'test_helper'
class OnlineNotificationTest < ActiveSupport::TestCase
role = Role.lookup( :name => 'Agent' )
group = Group.lookup( :name => 'Users' )
agent_user1 = User.create_or_update(
:login => 'agent_online_notify1',
:firstname => 'Bob',
:lastname => 'Smith',
:email => 'agent_online_notify1@example.com',
:password => 'some_pass',
:active => true,
:role_ids => [role.id],
:group_ids => [group.id],
:updated_by_id => 1,
:created_by_id => 1
)
agent_user2 = User.create_or_update(
:login => 'agent_online_notify2',
:firstname => 'Bob',
:lastname => 'Smith',
:email => 'agent_online_notify2@example.com',
:password => 'some_pass',
:active => true,
:role_ids => [role.id],
:group_ids => [group.id],
:updated_by_id => 1,
:created_by_id => 1
)
customer_user = User.lookup( :login => 'nicole.braun@zammad.org' )
test 'ticket notifiaction' do
tests = [
# test 1
{
:create => {
:ticket => {
:group_id => Group.lookup( :name => 'Users' ).id,
:customer_id => customer_user.id,
:owner_id => User.lookup( :login => '-' ).id,
:title => 'Unit Test 1 (äöüß)!',
:state_id => Ticket::State.lookup( :name => 'new' ).id,
:priority_id => Ticket::Priority.lookup( :name => '2 normal' ).id,
:updated_by_id => agent_user1.id,
:created_by_id => agent_user1.id,
},
:article => {
:updated_by_id => agent_user1.id,
:created_by_id => agent_user1.id,
:type_id => Ticket::Article::Type.lookup( :name => 'phone' ).id,
:sender_id => Ticket::Article::Sender.lookup( :name => 'Customer' ).id,
:from => 'Unit Test <unittest@example.com>',
:body => 'Unit Test 123',
:internal => false
},
},
:update => {
:ticket => {
:title => 'Unit Test 1 (äöüß) - update!',
:state_id => Ticket::State.lookup( :name => 'open' ).id,
:priority_id => Ticket::Priority.lookup( :name => '1 low' ).id,
:updated_by_id => customer_user.id,
},
},
:check => [
{
:type => 'create',
:object => 'Ticket',
:created_by_id => agent_user1.id,
},
{
:type => 'update',
:object => 'Ticket',
:created_by_id => customer_user.id,
},
]
},
]
tickets = []
tests.each { |test|
ticket = Ticket.create( test[:create][:ticket] )
test[:check][0][:o_id] = ticket.id
test[:check][1][:o_id] = ticket.id
test[:create][:article][:ticket_id] = ticket.id
article = Ticket::Article.create( test[:create][:article] )
assert_equal( ticket.class.to_s, 'Ticket' )
# execute ticket events
Observer::Ticket::Notification.transaction
#puts Delayed::Job.all.inspect
Delayed::Worker.new.work_off
# update ticket
if test[:update][:ticket]
ticket.update_attributes( test[:update][:ticket] )
end
# execute ticket events
Observer::Ticket::Notification.transaction
#puts Delayed::Job.all.inspect
Delayed::Worker.new.work_off
# remember ticket
tickets.push ticket
# check online notifications
notification_check( OnlineNotification.list(agent_user2, 10), test[:check] )
}
# delete tickets
tickets.each { |ticket|
ticket_id = ticket.id
ticket.destroy
found = Ticket.where( :id => ticket_id ).first
assert( !found, "Ticket destroyed")
}
end
def notification_check( onine_notifications, checks )
checks.each { |check_item|
hit = false
onine_notifications.each {|onine_notification|
if onine_notification['o_id'] == check_item[:o_id]
if onine_notification['object'] == check_item[:object]
if onine_notification['type'] == check_item[:type]
if onine_notification['created_by_id'] == check_item[:created_by_id]
hit = true
end
end
end
end
}
assert( hit, "online notification exists #{ check_item.inspect }" )
}
end
end

View file

@ -364,7 +364,7 @@ class TicketNotificationTest < ActiveSupport::TestCase
# create ticket in group
ticket1 = Ticket.create(
:title => 'some notification template test 1',
:title => 'some notification template test 1 Bobs\'s resumé',
:group => Group.lookup( :name => 'Users'),
:customer => customer,
:state => Ticket::State.lookup( :name => 'new' ),
@ -413,6 +413,16 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /updated/i, template[:subject] )
# en notification
subject = NotificationFactory.build(
:locale => agent2.preferences[:locale],
:string => template[:subject],
:objects => {
:ticket => ticket1,
:article => article,
:recipient => agent2,
}
)
assert_match( /Bobs's resumé/, subject )
body = NotificationFactory.build(
:locale => agent2.preferences[:locale],
:string => template[:body],
@ -437,6 +447,16 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /aktualis/, template[:subject] )
# de notification
subject = NotificationFactory.build(
:locale => agent1.preferences[:locale],
:string => template[:subject],
:objects => {
:ticket => ticket1,
:article => article,
:recipient => agent2,
}
)
assert_match( /Bobs's resumé/, subject )
body = NotificationFactory.build(
:locale => agent1.preferences[:locale],
:string => template[:body],