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) -> ws_send: (data) ->
App.Event.trigger( 'ws:send', JSON.stringify(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 class App.ControllerPermanent extends App.Controller
constructor: -> constructor: ->
super super

View file

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

View file

@ -205,29 +205,6 @@ class App.TicketCreate extends App.Controller
form_id: @form_id 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) => signatureChanges = (params, attribute, attributes, classname, form, ui) =>
if attribute && attribute.name is 'group_id' if attribute && attribute.name is 'group_id'
signature = undefined signature = undefined
@ -274,7 +251,7 @@ class App.TicketCreate extends App.Controller
events: events:
'change [name=customer_id]': @localUserInfo 'change [name=customer_id]': @localUserInfo
handlers: [ handlers: [
formChanges, @ticketFormChanges,
signatureChanges, signatureChanges,
] ]
filter: @form_meta.filter filter: @form_meta.filter
@ -297,7 +274,7 @@ class App.TicketCreate extends App.Controller
events: events:
'change [name=customer_id]': @localUserInfo 'change [name=customer_id]': @localUserInfo
handlers: [ handlers: [
formChanges, @ticketFormChanges,
signatureChanges, signatureChanges,
] ]
filter: @form_meta.filter filter: @form_meta.filter
@ -312,7 +289,7 @@ class App.TicketCreate extends App.Controller
events: events:
'change [name=customer_id]': @localUserInfo 'change [name=customer_id]': @localUserInfo
handlers: [ handlers: [
formChanges, @ticketFormChanges,
signatureChanges, signatureChanges,
] ]
filter: @form_meta.filter filter: @form_meta.filter

View file

@ -64,29 +64,6 @@ class Index extends App.ControllerContent
groupFilter = [groupFilter] groupFilter = [groupFilter]
@form_meta.filter.group_id = 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' ) @html App.view('customer_ticket_create')( head: 'New Ticket' )
new App.ControllerForm( new App.ControllerForm(
@ -95,7 +72,7 @@ class Index extends App.ControllerContent
model: App.Ticket model: App.Ticket
screen: 'create_top' screen: 'create_top'
handlers: [ handlers: [
formChanges @ticketFormChanges
] ]
filter: @form_meta.filter filter: @form_meta.filter
autofocus: true autofocus: true
@ -115,7 +92,7 @@ class Index extends App.ControllerContent
model: App.Ticket model: App.Ticket
screen: 'create_middle' screen: 'create_middle'
handlers: [ handlers: [
formChanges @ticketFormChanges
] ]
filter: @form_meta.filter filter: @form_meta.filter
params: defaults params: defaults

View file

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

View file

@ -267,29 +267,6 @@ class App.TicketZoom extends App.Controller
console.log('SHOW', ticket.id) console.log('SHOW', ticket.id)
el.find('.edit').html('') 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() defaults = ticket.attributes()
task_state = @taskGet('ticket') task_state = @taskGet('ticket')
modelDiff = @getDiff( defaults, task_state ) modelDiff = @getDiff( defaults, task_state )
@ -305,7 +282,7 @@ class App.TicketZoom extends App.Controller
screen: 'edit' screen: 'edit'
params: App.Ticket.find(ticket.id) params: App.Ticket.find(ticket.id)
handlers: [ handlers: [
formChanges @ticketFormChanges
] ]
filter: @form_meta.filter filter: @form_meta.filter
params: defaults params: defaults
@ -1534,12 +1511,18 @@ class ArticleView extends App.Controller
to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid to = customer.accounts['twitter'].username || customer.accounts['twitter'].uid
articleNew.to = to 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' if article.sender.name is 'Agent'
articleNew.to = article.to articleNew.to = article.to
else else
articleNew.to = article.from 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 # filter for uniq recipients
recipientAddresses = {} recipientAddresses = {}
recipient = emailAddresses.parseAddressList(articleNew.to) recipient = emailAddresses.parseAddressList(articleNew.to)
@ -1678,22 +1661,28 @@ class Article extends App.Controller
] ]
#if @article.type.name is 'note' #if @article.type.name is 'note'
# actions.push [] # 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 { actions.push {
name: 'reply' name: 'reply'
type: 'reply' type: 'reply'
href: '#' href: '#'
} }
recipients = [] recipients = []
if @article.sender.name is 'Agent'
if @article.to if @article.to
localRecipients = emailAddresses.parseAddressList(@article.to) localRecipients = emailAddresses.parseAddressList(@article.to)
if localRecipients if localRecipients
recipients = recipients.concat localRecipients recipients = recipients.concat localRecipients
else
if @article.from
localRecipients = emailAddresses.parseAddressList(@article.from)
if localRecipients
recipients = recipients.concat localRecipients
if @article.cc if @article.cc
localRecipients = emailAddresses.parseAddressList(@article.cc) localRecipients = emailAddresses.parseAddressList(@article.cc)
if localRecipients if localRecipients
recipients = recipients.concat localRecipients recipients = recipients.concat localRecipients
if recipients.length > 0 if recipients.length > 1
actions.push { actions.push {
name: 'reply all' name: 'reply all'
type: 'replyAll' 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>') @toggle.append('<div class="activity-counter">' + count.toString() + '</div>')
markAllAsRead: => markAllAsRead: =>
@counterUpdate(0)
@ajax( @ajax(
id: 'markAllAsRead' id: 'markAllAsRead'
type: 'POST' type: 'POST'
@ -63,14 +64,22 @@ class App.OnlineNotificationWidget extends App.Controller
onShow: => onShow: =>
@updateContent() @updateContent()
# set heigth of notification popover # set height of notification popover
height = $('#app').height() notificationsContainer = $('.js-notificationsContainer')
$('.js-notificationsContainer').css('height', "#{height-12}px") heightApp = $('#app').height()
$('.js-notificationsContainer .arrow').css('top', '20px') 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 # close notification list on click
$('.js-notificationsContainer').on('click', (e) => $('.js-notificationsContainer').on('click', (e) =>
#console.log('CL')
@hidePopover() @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="flex fullscreenMessage">
<div class="error icon"></div><h2><%- @T('Opps.. I\'m sorry, something went wrong!' ) %></h2> <div class="error icon"></div><h2><%- @T('Status Code') %>: <%= @status %>. <%= @detail %></h2>
<p><%- @T('Status Code') %>: <%= @status %></p>
<p><%= @detail %></p>
</div> </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/local_modal">Local Modal</a></li>
<li><a href="#layout_ref/loading_placeholder">Loading Placeholder</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/insufficient_rights">Insufficient Rights Warning</a></li>
<li><a href="#layout_ref/error">Error</a></li>
</ul> </ul>
</div> </div>

View file

@ -1,7 +1,7 @@
<% for item in @item_list: %> <% 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="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>
<div class="name u-textTruncate flex"><%= item.data.head %></div> <div class="name u-textTruncate flex"><%= item.data.head %></div>
<div class="closeTask js-close u-clickable horizontal centered" title="<%- @T('close') %>"> <div class="closeTask js-close u-clickable horizontal centered" title="<%- @T('close') %>">

View file

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

View file

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

View file

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

View file

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

View file

@ -103,7 +103,7 @@ returns
agents[ user.id ] = 1 agents[ user.id ] = 1
} }
dependencies = { :group_id => { '' => [] } } dependencies = { :group_id => { '' => { :owner_id => [] } } }
Group.where( :active => true ).each { |group| Group.where( :active => true ).each { |group|
assets = group.assets(assets) assets = group.assets(assets)
dependencies[:group_id][group.id] = { :owner_id => [] } dependencies[:group_id][group.id] = { :owner_id => [] }

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 end
caps = Selenium::WebDriver::Remote::Capabilities.send( browser ) caps = Selenium::WebDriver::Remote::Capabilities.send( browser )
caps.platform = ENV['BROWSER_OS'] || 'Windows 2008' if ENV['BROWSER_OS']
caps.version = ENV['BROWSER_VERSION'] || '8' caps.platform = ENV['BROWSER_OS']
end
if ENV['BROWSER_VERSION']
caps.version = ENV['BROWSER_VERSION']
end
local_browser = Selenium::WebDriver.for( local_browser = Selenium::WebDriver.for(
:remote, :remote,
:url => ENV['REMOTE_URL'], :url => ENV['REMOTE_URL'],
@ -257,18 +261,22 @@ class TestCase < Test::Unit::TestCase
if action[:timeout] if action[:timeout]
timeout = action[:timeout] timeout = action[:timeout]
end end
loops = (timeout / 2).to_i loops = (timeout).to_i
text = '' text = ''
(1..loops).each { |loop| (1..loops).each { |loop|
element = instance.find_elements( { :css => action[:area] } )[0] element = instance.find_elements( { :css => action[:area] } )[0]
if element #&& element.displayed? if element #&& element.displayed?
begin
text = element.text text = element.text
if text =~ /#{action[:value]}/i if text =~ /#{action[:value]}/i
assert( true, "(#{test[:name]}) '#{action[:value]}' found in '#{text}'" ) assert( true, "(#{test[:name]}) '#{action[:value]}' found in '#{text}'" )
return return
end end
rescue
# just try again
end end
sleep 2 end
sleep 1
} }
assert( false, "(#{test[:name]}) '#{action[:value]}' found in '#{text}'" ) assert( false, "(#{test[:name]}) '#{action[:value]}' found in '#{text}'" )
return return
@ -291,7 +299,6 @@ class TestCase < Test::Unit::TestCase
assert( false, "(#{test[:name]} / #{test[:area]}) still exsists" ) assert( false, "(#{test[:name]} / #{test[:area]}) still exsists" )
return return
elsif action[:execute] == 'create_user' elsif action[:execute] == 'create_user'
instance.find_elements( { :css => 'a[href="#manage"]' } )[0].click instance.find_elements( { :css => 'a[href="#manage"]' } )[0].click
instance.find_elements( { :css => 'a[href="#manage/users"]' } )[0].click instance.find_elements( { :css => 'a[href="#manage/users"]' } )[0].click
sleep 2 sleep 2
@ -329,6 +336,82 @@ class TestCase < Test::Unit::TestCase
assert( true, "(#{test[:name]}) user creation failed" ) assert( true, "(#{test[:name]}) user creation failed" )
return 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' elsif action[:execute] == 'verify_task_attributes'
if action[:title] if action[:title]
text = instance.find_elements( { :css => '.tasks .active' } )[0].text.strip text = instance.find_elements( { :css => '.tasks .active' } )[0].text.strip
@ -389,6 +472,11 @@ class TestCase < Test::Unit::TestCase
return return
end end
sleep 2 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] if action[:group]
element = instance.find_elements( { :css => '.active .newTicket select[name="group_id"]' } )[0] element = instance.find_elements( { :css => '.active .newTicket select[name="group_id"]' } )[0]
dropdown = Selenium::WebDriver::Support::Select.new(element) 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 # create ticket in group
ticket1 = Ticket.create( ticket1 = Ticket.create(
:title => 'some notification template test 1', :title => 'some notification template test 1 Bobs\'s resumé',
:group => Group.lookup( :name => 'Users'), :group => Group.lookup( :name => 'Users'),
:customer => customer, :customer => customer,
:state => Ticket::State.lookup( :name => 'new' ), :state => Ticket::State.lookup( :name => 'new' ),
@ -413,6 +413,16 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /updated/i, template[:subject] ) assert_match( /updated/i, template[:subject] )
# en notification # 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( body = NotificationFactory.build(
:locale => agent2.preferences[:locale], :locale => agent2.preferences[:locale],
:string => template[:body], :string => template[:body],
@ -437,6 +447,16 @@ class TicketNotificationTest < ActiveSupport::TestCase
assert_match( /aktualis/, template[:subject] ) assert_match( /aktualis/, template[:subject] )
# de notification # 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( body = NotificationFactory.build(
:locale => agent1.preferences[:locale], :locale => agent1.preferences[:locale],
:string => template[:body], :string => template[:body],