2012-05-24 17:45:29 +02:00
/ * *
2013-04-13 21:00:13 +02:00
* EGroupware eTemplate2 - JS History log
2012-05-24 17:45:29 +02:00
*
* @ license http : //opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @ package etemplate
* @ subpackage api
* @ link http : //www.egroupware.org
* @ author Nathan Gray
* @ copyright 2012 Nathan Gray
* @ version $Id$
* /
"use strict" ;
/ * e g w : u s e s
jquery . jquery ;
jquery . jquery - ui ;
et2 _core _valueWidget ;
// Include the grid classes
et2 _dataview ;
* /
/ * *
* eTemplate history log widget displays a list of changes to the current record .
* The widget is encapsulated , and only needs the record ' s ID , and a map of
2012-05-24 19:53:23 +02:00
* fields : widgets for display .
*
* It defers its initialization until the tab that it ' s on is selected , to avoid
* wasting time if the user never looks at it .
2013-04-13 21:00:13 +02:00
*
* @ augments et2 _valueWidget
2012-05-24 17:45:29 +02:00
* /
2013-04-13 21:00:13 +02:00
var et2 _historylog = et2 _valueWidget . extend ( [ et2 _IDataProvider ] ,
{
2012-07-24 01:54:16 +02:00
attributes : {
"value" : {
2013-06-17 23:22:32 +02:00
"name" : "Value" ,
"type" : "any" ,
"description" : "Object {app: ..., id: ..., status-widgets: {}} where status-widgets is a map of fields to widgets used to display those fields"
2012-07-25 22:56:05 +02:00
} ,
"status_id" : {
2013-06-17 23:22:32 +02:00
"name" : "status_id" ,
2012-07-25 22:56:05 +02:00
"type" : "string" ,
2013-06-17 23:22:32 +02:00
"default" : "status" ,
"description" : "The history widget is traditionally named 'status'. If you name another widget in the same template 'status', you can use this attribute to re-name the history widget. "
2012-07-24 01:54:16 +02:00
}
} ,
2012-07-25 22:56:05 +02:00
legacyOptions : [ "status_id" ] ,
2012-05-24 17:45:29 +02:00
columns : [
2012-05-30 20:54:23 +02:00
{ 'id' : 'user_ts' , caption : 'Date' , 'width' : '120px' , widget _type : 'date-time' } ,
2012-05-24 17:45:29 +02:00
{ 'id' : 'owner' , caption : 'User' , 'width' : '150px' , widget _type : 'select-account' } ,
{ 'id' : 'status' , caption : 'Changed' , 'width' : '120px' , widget _type : 'select' } ,
2012-07-25 22:56:05 +02:00
{ 'id' : 'new_value' , caption : 'New Value' , 'width' : 'auto' } ,
{ 'id' : 'old_value' , caption : 'Old Value' , 'width' : 'auto' }
2012-05-24 17:45:29 +02:00
] ,
2012-05-30 20:47:32 +02:00
TIMESTAMP : 0 , OWNER : 1 , FIELD : 2 , NEW _VALUE : 3 , OLD _VALUE : 4 ,
2013-04-13 21:00:13 +02:00
/ * *
* Constructor
*
* @ memberOf et2 _historylog
* /
2012-05-24 17:45:29 +02:00
init : function ( ) {
this . _super . apply ( this , arguments ) ;
this . div = $j ( document . createElement ( "div" ) )
. addClass ( "et2_historylog" ) ;
this . innerDiv = $j ( document . createElement ( "div" ) )
. appendTo ( this . div ) ;
} ,
2012-07-25 22:56:05 +02:00
set _status _id : function ( _new _id ) {
this . options . status _id = _new _id ;
} ,
2012-05-24 17:45:29 +02:00
doLoadingFinished : function ( ) {
this . _super . apply ( this , arguments ) ;
2012-05-24 19:53:23 +02:00
2012-05-24 17:45:29 +02:00
// Find the tab widget, if there is one
var tabs = this ;
do {
tabs = tabs . _parent ;
} while ( tabs != this . getRoot ( ) && tabs . _type != 'tabbox' ) ;
if ( tabs != this . getRoot ( ) )
{
// Find the tab index
for ( var i = 0 ; i < tabs . tabData . length ; i ++ )
{
// Find the tab
if ( tabs . tabData [ i ] . contentDiv . has ( this . div ) . length )
{
// Bind the action to when the tab is selected
var handler = function ( e ) {
e . data . div . unbind ( "click.history" ) ;
e . data . history . finishInit ( ) ;
e . data . history . dynheight . update ( function ( _w , _h ) {
e . data . history . dataview . resize ( _w , _h ) ;
} ) ;
} ;
tabs . tabData [ i ] . flagDiv . bind ( "click.history" , { "history" : this , div : tabs . tabData [ i ] . flagDiv } , handler ) ;
break ;
}
}
}
else
{
this . finishInit ( ) ;
}
} ,
2012-05-24 19:53:23 +02:00
/ * *
* Finish initialization which was skipped until tab was selected
* /
2012-05-24 17:45:29 +02:00
finishInit : function ( ) {
2013-07-20 19:20:55 +02:00
// No point with no ID
if ( ! this . options . value || ! this . options . value . id )
{
return ;
}
2012-06-13 17:13:49 +02:00
this . _filters = {
record _id : this . options . value . id ,
appname : this . options . value . app ,
get _rows : 'historylog::get_rows'
} ;
2012-05-24 17:45:29 +02:00
// Create the dynheight component which dynamically scales the inner
// container.
2013-11-28 19:43:44 +01:00
this . dynheight = new et2 _dynheight ( this . div . parent ( ) ,
2012-05-24 17:45:29 +02:00
this . innerDiv , 250
) ;
// Create the outer grid container
this . dataview = new et2 _dataview ( this . innerDiv , this . egw ( ) ) ;
2012-07-25 22:56:05 +02:00
var dataview _columns = [ ] ;
for ( var i = 0 ; i < this . columns . length ; i ++ )
{
dataview _columns [ i ] = { "id" : this . columns [ i ] . id , "caption" : this . columns [ i ] . caption , "width" : this . columns [ i ] . width } ;
}
this . dataview . setColumns ( dataview _columns ) ;
2012-05-24 17:45:29 +02:00
// Create widgets for columns that stay the same, and set up varying widgets
this . createWidgets ( ) ;
// Create the gridview controller
var linkCallback = function ( ) { } ;
this . controller = new et2 _dataview _controller ( null , this . dataview . grid ,
this , this . rowCallback , linkCallback , this ,
null
) ;
// Trigger the initial update
this . controller . update ( ) ;
// Write something inside the column headers
for ( var i = 0 ; i < this . columns . length ; i ++ )
{
$j ( this . dataview . getHeaderContainerNode ( i ) ) . text ( this . columns [ i ] . caption ) ;
}
// Register a resize callback
var self = this ;
$j ( window ) . resize ( function ( ) {
self . dynheight . update ( function ( _w , _h ) {
self . dataview . resize ( _w , _h ) ;
} ) ;
} ) ;
} ,
/ * *
* Destroys all
* /
destroy : function ( ) {
// Free the widgets
for ( var i = 0 ; i < this . columns . length ; i ++ )
{
if ( this . columns [ i ] . widget ) this . columns [ i ] . widget . destroy ( ) ;
}
for ( var key in this . fields )
{
this . fields [ key ] . widget . destroy ( ) ;
}
2012-05-30 01:05:26 +02:00
if ( this . diff ) this . diff . widget . destroy ( ) ;
2012-05-24 17:45:29 +02:00
// Free the grid components
2012-05-30 01:05:26 +02:00
if ( this . dataview ) this . dataview . free ( ) ;
if ( this . rowProvider ) this . rowProvider . free ( ) ;
if ( this . controller ) this . controller . free ( ) ;
if ( this . dynheight ) this . dynheight . free ( ) ;
2012-05-24 17:45:29 +02:00
this . _super . apply ( this , arguments ) ;
} ,
2012-07-09 21:04:32 +02:00
/ * *
* Create all needed widgets for new / o l d v a l u e s
* /
2012-05-24 17:45:29 +02:00
createWidgets : function ( ) {
// Constant widgets - first 3 columns
for ( var i = 0 ; i < this . columns . length ; i ++ )
{
if ( this . columns [ i ] . widget _type )
{
2012-07-25 22:56:05 +02:00
// Status ID is allowed to be remapped to something else. Only affects the widget ID though
var attrs = { 'readonly' : true , 'id' : ( i == this . FIELD ? this . options . status _id : this . columns [ i ] . id ) } ;
2012-05-24 17:45:29 +02:00
this . columns [ i ] . widget = et2 _createWidget ( this . columns [ i ] . widget _type , attrs , this ) ;
this . columns [ i ] . widget . transformAttributes ( attrs ) ;
this . columns [ i ] . nodes = $j ( this . columns [ i ] . widget . getDetachedNodes ( ) ) ;
}
}
2012-05-24 19:53:23 +02:00
// Add in handling for links
if ( typeof this . options . value [ 'status-widgets' ] [ '~link~' ] == 'undefined' )
{
2012-06-27 22:32:45 +02:00
this . columns [ this . FIELD ] . widget . optionValues [ '~link~' ] = this . egw ( ) . lang ( 'link' ) ;
2012-05-24 19:53:23 +02:00
this . options . value [ 'status-widgets' ] [ '~link~' ] = 'link' ;
}
2012-06-27 22:32:45 +02:00
// Add in handling for files
if ( typeof this . options . value [ 'status-widgets' ] [ '~file~' ] == 'undefined' )
{
this . columns [ this . FIELD ] . widget . optionValues [ '~file~' ] = this . egw ( ) . lang ( 'File' ) ;
this . options . value [ 'status-widgets' ] [ '~file~' ] = 'vfs' ;
}
2013-07-11 15:09:23 +02:00
// Add in handling for user-agent & action
if ( typeof this . options . value [ 'status-widgets' ] [ 'user_agent_action' ] == 'undefined' )
{
this . columns [ this . FIELD ] . widget . optionValues [ 'user_agent_action' ] = this . egw ( ) . lang ( 'User-agent & action' ) ;
}
2012-05-24 17:45:29 +02:00
// Per-field widgets - new value & old value
this . fields = { } ;
2012-05-30 20:47:32 +02:00
2012-07-25 22:56:05 +02:00
var labels = this . columns [ this . FIELD ] . widget . optionValues ;
2012-06-19 22:27:27 +02:00
// Custom fields - Need to create one that's all read-only for proper display
var cf _widget = et2 _createWidget ( 'customfields' , { 'readonly' : true } , this ) ;
cf _widget . loadFields ( ) ;
2012-06-19 23:49:58 +02:00
// Override this or it may damage the real values
cf _widget . getValue = function ( ) { return null ; } ;
2012-05-30 20:47:32 +02:00
for ( var key in cf _widget . widgets )
{
// Add label
labels [ cf _widget . prefix + key ] = cf _widget . options . customfields [ key ] . label ;
// If it doesn't support detached nodes, just treat it as text
if ( cf _widget . widgets [ key ] . getDetachedNodes )
{
var nodes = cf _widget . widgets [ key ] . getDetachedNodes ( ) ;
for ( var i = 0 ; i < nodes . length ; i ++ )
{
if ( nodes [ i ] == null ) nodes . splice ( i , 1 ) ;
}
2012-07-09 21:04:32 +02:00
// Save to use for each row
2012-05-30 20:47:32 +02:00
this . fields [ cf _widget . prefix + key ] = {
attrs : cf _widget . widgets [ key ] . options ,
widget : cf _widget . widgets [ key ] ,
nodes : jQuery ( nodes )
} ;
}
}
// Add all cf labels
this . columns [ this . FIELD ] . widget . set _select _options ( labels ) ;
// From app
2012-05-24 17:45:29 +02:00
for ( var key in this . options . value [ 'status-widgets' ] )
{
var field = this . options . value [ 'status-widgets' ] [ key ] ;
var attrs = { 'readonly' : true , 'id' : key } ;
2012-07-09 18:57:19 +02:00
var options = null ;
if ( typeof field == 'object' )
{
2012-07-25 23:12:04 +02:00
attrs [ 'select_options' ] = field ;
2012-07-09 18:57:19 +02:00
}
2012-07-09 21:04:32 +02:00
// Check for options after the type, ex: link-entry:infolog
2012-07-09 18:57:19 +02:00
else if ( field . indexOf ( ':' ) > 0 )
{
var options = field . split ( ':' ) ;
field = options . shift ( ) ;
}
2012-05-24 17:45:29 +02:00
var widget = et2 _createWidget ( typeof field == 'string' ? field : 'select' , attrs , this ) ;
2012-07-09 21:04:32 +02:00
// Parse / set legacy options
2012-07-09 18:57:19 +02:00
if ( options )
{
var mgr = this . getArrayMgr ( "content" ) ;
for ( var i = 0 ; i < options . length && i < widget . legacyOptions . length ; i ++ )
{
// Not set
if ( options [ i ] == "" ) continue ;
2012-07-09 21:04:32 +02:00
2012-07-09 18:57:19 +02:00
var attr = widget . attributes [ widget . legacyOptions [ i ] ] ;
var attrValue = options [ i ] ;
// If the attribute is marked as boolean, parse the
// expression as bool expression.
if ( attr . type == "boolean" )
{
attrValue = mgr . parseBoolExpression ( attrValue ) ;
}
else
{
attrValue = mgr . expandName ( attrValue ) ;
}
attrs [ widget . legacyOptions [ i ] ] = attrValue ;
if ( typeof widget [ 'set_' + widget . legacyOptions [ i ] ] == 'function' )
{
widget [ 'set_' + widget . legacyOptions [ i ] ] . call ( widget , attrValue ) ;
}
else
{
widget . options [ widget . legacyOptions [ i ] ] = attrValue ;
}
}
}
2012-07-05 21:59:42 +02:00
if ( widget . instanceOf ( et2 _selectbox ) ) widget . options . multiple = true ;
2012-05-24 17:45:29 +02:00
widget . transformAttributes ( attrs ) ;
2012-07-09 21:04:32 +02:00
// Save to use for each row
2012-05-24 17:45:29 +02:00
this . fields [ key ] = {
attrs : attrs ,
widget : widget ,
nodes : jQuery ( widget . getDetachedNodes ( ) )
} ;
}
// Widget for text diffs
var diff = et2 _createWidget ( 'diff' , { } , this ) ;
this . diff = {
widget : diff ,
nodes : jQuery ( diff . getDetachedNodes ( ) )
} ;
} ,
getDOMNode : function ( _sender ) {
if ( _sender == this )
{
return this . div [ 0 ] ;
}
for ( var i = 0 ; i < this . columns . length ; i ++ )
{
if ( _sender == this . columns [ i ] . widget )
{
return this . dataview . getHeaderContainerNode ( i ) ;
}
}
return null ;
} ,
dataFetch : function ( _queriedRange , _callback , _context ) {
2013-07-17 11:36:38 +02:00
// Skip getting data if there's no ID
if ( ! this . value . id ) return ;
2012-05-24 17:45:29 +02:00
// Pass the fetch call to the API
2013-07-17 11:36:38 +02:00
this . egw ( ) . dataFetch (
2012-05-24 17:45:29 +02:00
this . getInstanceManager ( ) . etemplate _exec _id ,
_queriedRange ,
this . _filters ,
this . id ,
_callback ,
_context
) ;
} ,
// Needed by interface
dataRegisterUID : function ( _uid , _callback , _context ) {
this . egw ( ) . dataRegisterUID ( _uid , _callback , _context , this . getInstanceManager ( ) . etemplate _exec _id ,
this . id ) ;
} ,
dataUnregisterUID : function ( _uid , _callback , _context ) {
// Needed by interface
} ,
/ * *
* The row callback gets called by the gridview controller whenever
* the actual DOM - Nodes for a node with the given data have to be
* created .
* /
rowCallback : function ( _data , _row , _idx , _entry ) {
var tr = _row . getDOMNode ( ) ;
jQuery ( tr ) . attr ( "valign" , "top" ) ;
var row = this . dataview . rowProvider . getPrototype ( "default" ) ;
var self = this ;
$j ( "div" , row ) . each ( function ( i ) {
var nodes = [ ] ;
var widget = self . columns [ i ] . widget ;
if ( typeof widget == 'undefined' && typeof self . fields [ _data . status ] != 'undefined' )
{
nodes = self . fields [ _data . status ] . nodes . clone ( ) ;
widget = self . fields [ _data . status ] . widget ;
}
else if ( widget )
{
nodes = self . columns [ i ] . nodes . clone ( ) ;
}
2012-06-26 01:38:26 +02:00
else if ( self . _needsDiffWidget ( _data [ 'status' ] , _data [ self . columns [ self . OLD _VALUE ] . id ] ) ||
self . _needsDiffWidget ( _data [ 'status' ] , _data [ self . columns [ self . NEW _VALUE ] . id ] ) )
2012-05-24 17:45:29 +02:00
{
2012-05-24 19:53:23 +02:00
// Large text value - span both columns, and show a nice diff
2012-05-24 17:45:29 +02:00
var jthis = jQuery ( this ) ;
2012-07-09 21:04:32 +02:00
if ( i == self . NEW _VALUE )
2012-05-24 17:45:29 +02:00
{
2012-06-05 17:48:57 +02:00
// Diff widget
2012-05-24 17:45:29 +02:00
widget = self . diff . widget ;
nodes = self . diff . nodes . clone ( ) ;
_data [ self . columns [ i ] . id ] = {
'old' : _data [ self . columns [ i + 1 ] . id ] ,
'new' : _data [ self . columns [ i ] . id ]
} ;
// Skip column 4
jthis . parents ( "td" ) . attr ( "colspan" , 2 )
. css ( "border-right" , "none" ) ;
2012-06-26 01:38:26 +02:00
jthis . css ( "width" , ( self . dataview . columnMgr . columnWidths [ i ] + self . dataview . columnMgr . columnWidths [ i + 1 ] - 10 ) + 'px' ) ;
2012-05-24 17:45:29 +02:00
if ( widget ) widget . setDetachedAttributes ( nodes , {
value : _data [ self . columns [ i ] . id ] ,
label : jthis . parents ( "td" ) . prev ( ) . text ( )
} ) ;
2012-06-26 01:38:26 +02:00
2012-05-24 17:45:29 +02:00
// Skip column 4
2012-06-26 01:38:26 +02:00
jthis . parents ( "td" ) . next ( ) . remove ( ) ;
2012-05-24 17:45:29 +02:00
}
}
else
{
2012-05-24 19:53:23 +02:00
// No widget fallback - display actual value
2012-05-24 17:45:29 +02:00
nodes = '<span>' + _data [ self . columns [ i ] . id ] + '</span>' ;
}
if ( widget ) widget . setDetachedAttributes ( nodes , { value : _data [ self . columns [ i ] . id ] } ) ;
$j ( this ) . append ( nodes ) ;
} ) ;
$j ( tr ) . append ( row . children ( ) ) ;
return tr ;
} ,
/ * *
* How to tell if the row needs a diff widget or not
* /
_needsDiffWidget : function ( columnName , value ) {
2012-06-26 01:38:26 +02:00
if ( typeof value !== "string" )
{
this . egw ( ) . debug ( "warning" , "Crazy diff value" , value ) ;
return false ;
}
2013-04-13 21:00:13 +02:00
return columnName == 'note' || columnName == 'description' || ( value && ( value . length > 50 || value . match ( /\n/g ) ) ) ;
2012-05-24 17:45:29 +02:00
} ,
} ) ;
et2 _register _widget ( et2 _historylog , [ 'historylog' ] ) ;