Fixed correct expanding of names and implemented the dataProvider class

This commit is contained in:
Andreas Stöckel 2011-09-08 18:36:09 +00:00
parent e151398d94
commit 87c0db7be1
19 changed files with 526 additions and 101 deletions

View File

@ -61,14 +61,14 @@ var et2_arrayMgr = Class.extend({
return this;
},
getValueForID : function(_id) {
/* getValueForID : function(_id) {
if (typeof this.data[_id] != "undefined")
{
return this.data[_id];
}
return null;
},
},*/
/**
* Returns the path to this content array manager perspective as an array
@ -306,7 +306,6 @@ var et2_readonlysArrayMgr = et2_arrayMgr.extend({
// If the attribute is set, return that
if (typeof _attr != "undefined" && _attr !== null)
{
console.log(_attr, et2_evalBool(_attr));
return et2_evalBool(_attr);
}

View File

@ -335,6 +335,32 @@ function et2_arrayValues(_arr)
return result;
}
/**
* Equivalent to the PHP array_keys function
*/
function et2_arrayKeys(_arr)
{
var result = [];
for (var key in _arr)
{
result.push(key);
}
return result;
}
function et2_arrayIntKeys(_arr)
{
var result = [];
for (var key in _arr)
{
result.push(parseInt(key));
}
return result;
}
/**
* Equivalent to the PHP substr function, partly take from phpjs, licensed under
* the GPL.

View File

@ -80,7 +80,7 @@ var et2_inputWidget = et2_valueWidget.extend(et2_IInput, {
// Check whether an validation error entry exists
if (this.id)
{
var val = this.getArrayMgr("validation_errors").getValueForID(this.id);
var val = this.getArrayMgr("validation_errors").getEntry(this.id);
if (val)
{
_attrs["validation_error"] = val;

View File

@ -119,4 +119,3 @@ var et2_IDetachedDOM = new Interface({
});

View File

@ -343,12 +343,14 @@
}
var parts = [];
var hasString = false;
for (var i = 0; i < _string.length; i++)
{
var part = _string[i];
if (typeof part == "string")
{
hasString = true;
// Escape all "'" and "\" chars and add the string to the parts array
parts.push("'" + part.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'");
}
@ -358,7 +360,7 @@
}
}
if (parts.length == 0)
if (!hasString) // Force the result to be of the type string
{
parts.push('""');
}
@ -433,6 +435,7 @@
test("${row}[title]", "10[title]");
test("{$row_cont[title]}", "Hello World!");
test('{$cont["$row"][\'title\']}', "Hello World!");
test("$row_cont[${row}[title]]");
test("\\\\", "\\");
test("", "");
})();*/

View File

@ -41,7 +41,7 @@ var et2_valueWidget = et2_baseWidget.extend({
// Set the value for this element
var contentMgr = this.getArrayMgr("content");
if (contentMgr != null) {
var val = contentMgr.getValueForID(this.id);
var val = contentMgr.getEntry(this.id);
if (val !== null)
{
_attrs["value"] = val;

View File

@ -510,7 +510,12 @@ var et2_widget = Class.extend({
// Apply the content of the modifications array
if (this.id)
{
var data = this.getArrayMgr("modifications").getValueForID(this.id);
if (typeof this.id != "string")
{
console.log(this.id);
}
var data = this.getArrayMgr("modifications").getEntry(this.id);
if (data instanceof Object)
{
for (var key in data)
@ -769,7 +774,7 @@ var et2_widget = Class.extend({
this._template_application = this.getParent().getTemplateApp();
return this._template_application;
}
return null;
return "phpgwapi";
}
});

View File

@ -33,3 +33,49 @@ var et2_dataview_IViewRange = new Interface({
setViewRange: function(_range) {}
});
/**
* Interface which objects have to implement, that want to act as low level
* datasource.
*/
var et2_IRowFetcher = new Interface({
/**
* @param _fetchList is an array consisting of objects whith the entries
* "startIdx" and "count"
* @param _callback is the callback which is called when the data is ready
* (may be immediately or deferred). The callback has the following
* signature:
* function (_rows)
* where _rows is an associative array which contains the data for that row.
* @param _context is the context in which the callback should run.
*/
getRows: function(_fetchList, _callback, _context) {}
});
/**
* Interface the data provider has to implement
*/
var et2_IDataProvider = new Interface({
/**
* Returns the total count of grid elements
*/
getCount: function() {},
/**
* Registers the given dataRow for the given index. Calls _dataRow.updateData
* as soon as data is available for that row.
*/
registerDataRow: function(_dataRow, _idx) {},
/**
* Stops calling _dataRow.updateData for the dataRow registered for the given
* index.
*/
unregisterDataRow: function(_idx) {}
});

View File

@ -13,49 +13,234 @@
"use strict"
/*egw:uses
et2_inheritance;
et2_core_inheritance;
et2_core_common;
et2_dataview_interfaces;
*/
var et2_dataview_dataProvider = Class.extend({
var et2_dataview_dataProvider = Class.extend(et2_IDataProvider, {
init: function() {
this.updateQueue = 0;
/**
* Creates this instance of the data provider.
*/
init: function(_source, _total) {
this._source = _source;
this._total = _total;
this._registeredRows = {};
this._data = {};
this._dataCount = 0;
this._queue = {};
this._queueSize = 0;
this._stepSize = 25; // Count of elements which is loaded at once
this._maxCount = 1000; // Maximum count before the elements are cleaned up
var self = this;
this._cleanupInterval = window.setInterval(function() {self._cleanup()},
10 * 1000);
this._queueFlushTimeout = null;
},
destroy: function() {
// Destroy the cleanup timer callback
window.clearInterval(this._cleanupInterval);
// Destroy the _queueFlushTimeout
if (this._queueFlushTimeout !== null)
{
window.clearTimeout(this._queueFlushTimeout);
}
},
/**
* Returns the total count
*/
getCount: function() {
return 10000;
return this._total;
},
registerDataRow: function(_dataRow, _idx) {
/* var row = {
"type": "dataRow",
"data": {
"ts_title": "Row " + _idx
}
};
// Make sure _idx is a int
_idx = parseInt(_idx);
// Get a random value which is used to simulate network latency and time
// it needs to load the data.
var rnd = Math.round(Math.random() * 1000);
et2_debug("log", "--> registering row", _idx);
if (rnd < 200)
if (typeof this._registeredRows[_idx] != "undefined")
{
_dataRow.updateData(row);
et2_debug("warn", "Overriding data row for index " + _idx);
}
window.setTimeout(function() {_dataRow.updateData(row); },
Math.round(rnd / 2));*/
// Associate the given data row with that index
this._registeredRows[_idx] = _dataRow;
// All data rows are updated independently of all others - this allows
// user input between generation of the widgets.
//window.setTimeout(function() {_dataRow.updateData({"readonlys": {"__ALL__": true}});}, 0);
_dataRow.updateData({"content": {"ts_title": "Idx: " + _idx}});
// Check whether an entry exists in the data array - if yes, call the
// request immediately
if (typeof this._data[_idx] != "undefined")
{
this._callUpdateData(_idx);
}
else
{
this._queueIndex(_idx);
}
},
unregisterDataRow: function(_dataRow) {
//
unregisterDataRow: function(_idx) {
// Make sure _idx is a int
_idx = parseInt(_idx);
et2_debug("log", "<-- unregistering row", _idx);
delete(this._data[_idx]);
},
/* ---- PRIVATE FUNCTIONS ---- */
_queueIndex: function(_idx) {
// Mark the index as queued
if (typeof this._queue[_idx] == "undefined")
{
this._queue[_idx] = true;
this._queueSize++;
}
if (this._queueSize > this._stepSize)
{
this._flushQueue();
}
else
{
// (Re)start the queue flush timer
var self = this;
this._stopFlushTimer();
this._queueFlushTimeout = window.setTimeout(function() {
self._queueFlushTimeout = null;
self._flushQueue();
}, 50);
}
},
_flushQueue: function() {
// Stop the flush timer if it is still active
this._stopFlushTimer();
// Mark all elements in a radius of this._stepSize / 2
var marked = {};
var r = Math.floor(this._stepSize / 2);
for (var key in this._queue)
{
key = parseInt(key);
var b = Math.max(0, key - r);
var t = Math.min(key + r, this._total - 1);
for (var i = b; i <= t; i ++)
{
marked[i] = true;
}
}
// Reset the queue
this._queue = {};
this._queueSize = 0;
// Create a list with start indices and counts
var fetchList = [];
var entry = null;
var last = 0;
// Get the int keys and sort the array numeric
var arr = et2_arrayIntKeys(marked).sort(function(a,b){return a > b ? 1 : (a == b ? 0 : -1)});
for (var i = 0; i < arr.length; i++)
{
if (i == 0 || arr[i] - last > 1)
{
if (entry)
{
fetchList.push(entry);
}
entry = {
"startIdx": arr[i],
"count": 1
};
}
else
{
entry.count++;
}
last = arr[i];
}
if (entry)
{
fetchList.push(entry);
}
// Call the "getRows" callback
this._source.getRows(fetchList, this._receiveData, this);
},
_receiveData: function(_data) {
var time = (new Date).getTime();
for (var key in _data)
{
// Make sure the key is a int
key = parseInt(key);
// Copy the data for the given index
this._data[key] = {
"data": _data[key],
"timestamp": time
};
// Update the row associated to the index
this._callUpdateData(key);
}
},
_stopFlushTimer: function() {
// Stop the queue flush timer
if (this._queueFlushTimeout !== null)
{
window.clearTimeout(this._queueFlushTimeout);
}
},
_callUpdateData: function(_idx) {
if (typeof this._registeredRows[_idx] != "undefined")
{
// this._data[idx].timestamp = (new Date).getTime();
this._registeredRows[_idx].updateData({
"content": this._data[_idx].data
});
}
},
_cleanup: function() {
// Delete all data rows which have not been accessed for more than
// "delta" ms (5 minutes) - this method does not ensure that _dataCount
// gets below _maxCount!
var delta = 5 * 60 * 1000;
var now = (new Date).getTime();
if (this._dataCount > this._maxCount)
{
for (var key in this._data)
{
var entry = this._data[key];
if (now - entry.timestamp > delta)
{
delete(this._data[key]);
this._dataCount--;
}
}
}
}
});

View File

@ -101,6 +101,12 @@ var et2_dataview_partitionNode = Class.extend([et2_dataview_IPartitionHeight,
// Invalidate the parent node
if (this._parent)
{
// Invalidate the neighbor node
if (this._pidx < this._parent._children.length - 1)
{
this._parent._children[this._pidx + 1].invalidate();
}
this._parent.invalidate(origin ? this : _sender);
}
}

View File

@ -23,6 +23,7 @@ var et2_dataview_row = et2_dataview_container.extend(et2_dataview_IDataRow, {
this._super(_dataProvider, _rowProvider, _invalidationElem);
this._avgHeight = _avgHeight;
this._idx = null;
this.rowWidget = null;
this.hasAvgHeight = false;
@ -37,7 +38,10 @@ var et2_dataview_row = et2_dataview_container.extend(et2_dataview_IDataRow, {
destroy: function() {
// Unregister the row from the data provider
this.dataProvider.unregisterDataRow(this);
if (this._idx !== null)
{
this.dataProvider.unregisterDataRow(this._idx);
}
// Free the row widget first, if it has been set
if (this.rowWidget)

View File

@ -76,17 +76,6 @@ var et2_dataview_rowProvider = Class.extend({
"data": []
};
// Include the "value" attribute if the widget is derrived from
// et2_valueWidget
if (_widget instanceof et2_valueWidget)
{
hasAttr = true;
widgetData.data.push({
"attribute": "value",
"expression": "@${row}"
});
}
// Get all attribute values
for (var key in _widget.attributes)
{
@ -166,13 +155,22 @@ var et2_dataview_rowProvider = Class.extend({
// "detached" mode
var supportedAttrs = [];
widget.getDetachedAttributes(supportedAttrs);
supportedAttrs.push("id");
isDetachable = true;
for (var j = 0; j < _varAttrs[i].data.length && isDetachable; j++)
for (var j = 0; j < _varAttrs[i].data.length/* && isDetachable*/; j++)
{
var data = _varAttrs[i].data[j];
isDetachable &= supportedAttrs.indexOf(data.attribute) != -1;
var supportsAttr = supportedAttrs.indexOf(data.attribute) != -1;
if (!supportsAttr)
{
et2_debug("warn", "et2_IDetachedDOM widget " +
widget._type + " does not support " + data.attribute);
}
isDetachable &= supportsAttr;
}
}
@ -294,7 +292,7 @@ var et2_dataview_rowProvider = Class.extend({
// Record the path to each DOM-Node
for (var j = 0; j < nodes.length; j++)
{
nodeFuncs[i] = this._compileDOMAccessFunc(_rowTemplate.row,
nodeFuncs[j] = this._compileDOMAccessFunc(_rowTemplate.row,
nodes[j]);
}
}
@ -320,7 +318,7 @@ var et2_dataview_rowProvider = Class.extend({
};
// Create the row widget and insert the given widgets into the row
var rowWidget = new et2_dataview_rowWidget(row[0]);
var rowWidget = new et2_dataview_rowWidget(_rootWidget, row[0]);
rowWidget.createWidgets(_widgets);
// Get the set containing all variable attributes
@ -354,6 +352,18 @@ var et2_dataview_rowProvider = Class.extend({
var rowWidget = null;
if (this._template.seperated.remaining.length > 0)
{
// Transform the variable attributes
for (var i = 0; i < this._template.seperated.remaining.length; i++)
{
var entry = this._template.seperated.remaining[i];
for (var j = 0; j < entry.data.length; j++)
{
var set = entry.data[j];
entry.widget.options[set.attribute] = mgrs["content"].expandName(set.expression);
}
}
// Create the row widget
var rowWidget = new et2_dataview_rowTemplateWidget(this._rootWidget,
_row[0]);
@ -371,7 +381,7 @@ var et2_dataview_rowProvider = Class.extend({
var data = {};
for (var j = 0; j < entry.data.length; j++)
{
var set = entry.data[i];
var set = entry.data[j];
data[set.attribute] = mgrs["content"].expandName(set.expression);
}
@ -384,6 +394,13 @@ var et2_dataview_rowProvider = Class.extend({
nodes[j] = entry.nodeFuncs[j](_row[0]);
}
// Set the array managers first
entry.widget._mgrs = mgrs;
if (typeof data.id != "undefined")
{
entry.widget.id = data.id;
}
// Call the setDetachedAttributes function
entry.widget.setDetachedAttributes(nodes, data);
}
@ -431,9 +448,9 @@ var et2_dataview_rowProvider = Class.extend({
var et2_dataview_rowWidget = et2_widget.extend(et2_IDOMNode, {
init: function(_row) {
init: function(_parent, _row) {
// Call the parent constructor with some dummy attributes
this._super(null, {"id": "", "type": "rowWidget"});
this._super(_parent, {"id": "", "type": "rowWidget"});
// Initialize some variables
this._widgets = [];

View File

@ -71,7 +71,7 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, {
// Create the data provider which cares about streaming the row data
// efficiently to the rows
this.dataProvider = new et2_dataview_dataProvider();
this.dataProvider = new et2_dataview_dataProvider(this, 100);
// Create the outer grid container
this.dataviewContainer = new et2_dataview_gridContainer(this.div,
@ -101,6 +101,26 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, {
}, this);
},
/**
* Get Rows callback
*/
getRows: function(_fetchList, _callback, _context) {
console.log("Nextmatch will fetch ", _fetchList);
// Create the entries:
var entries = {};
for (var i = 0; i < _fetchList.length; i++)
{
var start = _fetchList[i].startIdx;
for (var j = 0; j < _fetchList[i].count; j++)
{
entries[start + j] =
{"info_id":"5","info_type":"email","info_from":"tracker","info_addr":"tracker","info_subject":"InfoLog grid view: problem with Opera; 'permission denied' while saving","info_des":"<snip>","info_owner":"5","info_responsible":[],"info_access":"public","info_cat":"0","info_datemodified":1307112528,"info_startdate":1306503000,"info_enddate":"0", "info_id_parent":"0","info_planned_time":"0","info_replanned_time":"0","info_used_time":"0","info_status":"done", "info_confirm":"not","info_modifier":"5","info_link_id":0,"info_priority":"1","pl_id":"0","info_price":null, "info_percent":"100%","info_datecompleted":1307112528,"info_location":"","info_custom_from":1, "info_uid":"infolog-5-18d12c7bf195f6b9d602e1fa5cde28f1","info_cc":"","caldav_name":"5.ics","info_etag":"0", "info_created":1307112528,"info_creator":"5","links":[],"info_anz_subs":0,"sub_class":"normal_done","info_link":{"title":"tracker"},"class":"rowNoClose rowNoCloseAll ","info_type_label":"E-Mail","info_status_label":"done","info_number":"5"}
}
}
_callback.call(_context, entries);
},
/**
* Sorts the nextmatch widget by the given ID.
*
@ -253,7 +273,6 @@ var et2_nextmatch = et2_DOMWidget.extend(et2_IResizeable, {
/**
* When the template attribute is set, the nextmatch widget tries to load
* that template and to fetch the grid which is inside of it. It then calls
* _parseGrid in order to get the information for the column headers etc.
*/
set_template: function(_value) {
if (!this.template)

View File

@ -81,9 +81,6 @@ var et2_description = et2_baseWidget.extend([et2_IDetachedDOM], {
init: function() {
this._super.apply(this, arguments);
this.value = "";
this.font_style = "";
// Create the span/label tag which contains the label text
this.span = $j(document.createElement(this.options["for"] ? "label" : "span"))
.addClass("et2_label");
@ -100,6 +97,20 @@ var et2_description = et2_baseWidget.extend([et2_IDetachedDOM], {
this.setDOMNode(this.span[0]);
},
transformAttributes: function(_attrs) {
this._super.apply(arguments);
if (this.id)
{
var val = this.getArrayMgr("content").getEntry(this.id);
if (val)
{
_attrs["value"] = val;
}
}
},
_parseText: function(_value) {
if (this.options.href)
{
@ -131,7 +142,7 @@ var et2_description = et2_baseWidget.extend([et2_IDetachedDOM], {
getDetachedAttributes: function(_attrs)
{
_attrs.push("value");
_attrs.push("value", "class");
},
getDetachedNodes: function()
@ -141,11 +152,18 @@ var et2_description = et2_baseWidget.extend([et2_IDetachedDOM], {
setDetachedAttributes: function(_nodes, _values)
{
this.transformAttributes(_values);
if (typeof _values["value"] != "undefined")
{
et2_insertLinkText(this._parseText(_values["value"]), _nodes[0],
this.options.extra_link_target);
}
if (typeof _values["class"] != "undefined")
{
this.set_class(_values["class"]);
}
}
});

View File

@ -14,13 +14,14 @@
/*egw:uses
jquery.jquery;
et2_core_interfaces;
et2_core_baseWidget;
*/
/**
* Class which implements the "image" XET-Tag
*/
var et2_image = et2_baseWidget.extend({
var et2_image = et2_baseWidget.extend(/*et2_IDetachedDOM,*/ {
attributes: {
"src": {
@ -44,17 +45,31 @@ var et2_image = et2_baseWidget.extend({
this._super.apply(this, arguments);
// Create the image or a/image tag
this.image = this.node = $j(document.createElement("img"));
var node = this.image = $j(document.createElement("img"));
if(this.options.link)
{
this.node = $j(document.createElement("a"));
this.image.appendTo(this.node);
this._node = $j(document.createElement("a"));
this.image.appendTo(node);
}
if(this.options["class"])
{
this.node.addClass(this.options["class"]);
node.addClass(this.options["class"]);
}
this.setDOMNode(node[0]);
},
transformAttributes: function(_attrs) {
this._super.apply(arguments);
// Check to expand name
if (typeof _attrs["src"] != "undefined")
{
var src = this.getArrayMgr("content").getEntry(_attrs["src"]);
if (src)
{
_attrs["src"] = src;
}
}
this.setDOMNode(this.node[0]);
},
set_label: function(_value) {
@ -62,7 +77,7 @@ var et2_image = et2_baseWidget.extend({
this.options.label = _value;
// label is NOT the alt attribute in eTemplate, but the title/tooltip
this.image.attr("alt", _value);
this.image.set_statustext(_value);
this.set_statustext(_value);
},
setValue: function(_value) {
@ -70,36 +85,69 @@ var et2_image = et2_baseWidget.extend({
this.set_src(_value);
},
percentagePreg: /^[0-9]+%$/,
set_src: function(_value) {
if(!this.isInTree())
{
return;
}
// Check to expand name
if(_value.indexOf("$") != -1 || _value.indexOf("@") != -1) {
var contentMgr = this.getArrayMgr("content");
if (contentMgr != null) {
var val = contentMgr.getValueForID(_value);
if (val !== null)
{
_value = val;
}
}
}
this.options.src = _value;
// Get application to use from template ID
var appname = this.getTemplateApp();
var src = egw.image(_value,appname || "phpgwapi");
if(src )
// Check whether "src" is a percentage
if (this.percentagePreg.test(_value))
{
this.image.attr("src", src).show();
this.getSurroundings().prependDOMNode(document.createTextNode(_value));
}
else
{
this.image.css("display","none");
// Get application to use from template ID
var src = egw.image(_value, this.getTemplateApp());
if(src)
{
this.image.attr("src", src).show();
}
else
{
this.image.css("display","none");
}
}
}
},
/**
* Implementation of "et2_IDetachedDOM" for fast viewing in gridview
*/
// Does currently not work for percentages, as the surroundings manager
// cannot opperate on other DOM-Nodes.
/* getDetachedAttributes: function(_attrs) {
_attrs.push("src", "label");
},
getDetachedNodes: function() {
return [this.node, this.image[0]];
},
setDetachedAttributes: function(_nodes, _values) {
// Set the given DOM-Nodes
this.node = _nodes[0];
this.image = $j(_nodes[1]);
this.transformAttributes(_values);
// Set the attributes
if (_values["src"])
{
this.set_src(_values["src"]);
}
if (_values["label"])
{
this.set_label(_values["label"]);
}
}*/
});
et2_register_widget(et2_image, ["image"]);

View File

@ -118,7 +118,7 @@ var et2_link_to = et2_inputWidget.extend({
if (_attrs["select_options"] == null)
{
_attrs["select_options"] = this.getArrayMgr('content')
.getValueForID("options-" + this.id)
.getEntry("options-" + this.id)
}
// Default to an empty object

View File

@ -77,14 +77,14 @@ var et2_selectbox = et2_inputWidget.extend({
this._super.apply(this, arguments);
// Try to find the options inside the "sel-options" array
_attrs["select_options"] = this.getArrayMgr("sel_options").getValueForID(this.id);
_attrs["select_options"] = this.getArrayMgr("sel_options").getEntry(this.id);
// Check whether the options entry was found, if not read it from the
// content array.
if (_attrs["select_options"] == null)
{
_attrs["select_options"] = this.getArrayMgr('content')
.getValueForID("options-" + this.id)
.getEntry("options-" + this.id)
}
// Default to an empty object

View File

@ -64,7 +64,7 @@ var et2_tabbox = et2_DOMWidget.extend({
// Set the value for this element
var contentMgr = this.getArrayMgr("content");
if (contentMgr != null) {
var val = contentMgr.getValueForID(this.id);
var val = contentMgr.getEntry(this.id);
if (val !== null)
{
selected = val;
@ -72,7 +72,7 @@ var et2_tabbox = et2_DOMWidget.extend({
}
contentMgr = this.getArrayMgr("readonlys");
if (contentMgr != null) {
var val = contentMgr.getValueForID(this.id);
var val = contentMgr.getEntry(this.id);
if (val !== null)
{
hidden = val;

View File

@ -63,19 +63,69 @@
<nextmatch-sortheader label="last changed" id="info_datemodified" options="DESC"/>
</row>
<row class="$row_cont[info_cat] $row_cont[class]" valign="top">
<vbox>
<description value="Dynamic description tag:" />
<description value="$cont[$row][ts_title]" />
<button label="This is a button" />
<hbox align="center" options="5">
<image label="$row_cont[info_type]" src="${row}[info_type]"/>
<button statustext="Change the status of an entry, eg. close it" label="$row_cont[info_status_label]" id="edit_status[$row_cont[info_id]]" onclick="window.open(egw::link('/index.php','menuaction=infolog.infolog_ui.edit&amp;info_id=$row_cont[info_id]'),'_blank','dependent=yes,width=750,height=600,scrollbars=yes,status=yes'); return false;" image="$row_cont[info_status_label]" ro_image="$row_cont[info_status_label]"/>
<button statustext="Change the status of an entry, eg. close it" label="$row_cont[info_percent]" id="edit_percent[$row_cont[info_id]]" onclick="window.open(egw::link('/index.php','menuaction=infolog.infolog_ui.edit&amp;info_id=$row_cont[info_id]'),'_blank','dependent=yes,width=750,height=600,scrollbars=yes,status=yes'); return false;" image="$row_cont[info_percent]"/>
<image label="$row_cont[info_percent2]" src="{$row}[info_percent2]" onclick="window.open(egw::link('/index.php','menuaction=infolog.infolog_ui.edit&amp;info_id=$row_cont[info_id]'),'_blank','dependent=yes,width=750,height=600,scrollbars=yes,status=yes'); return false;"/>
</hbox>
<vbox options="0,0" class="fullWidth">
<link label="%s $row_cont[info_addr]" id="${row}[info_link]" options="b"/>
<hbox options="0,0">
<description id="${row}[info_subject]" no_lang="1" class="$row_cont[sub_class]"/>
<description align="right" id="{$row}[info_number]" no_lang="1" class="infoId"/>
</hbox>
<box class="infoDes">
<description id="${row}[info_des]" no_lang="1" options=",,1"/>
</box>
<link-string id="${row}[filelinks]"/>
</vbox>
<customfields-list id="$row" class="customfields"/>
<menulist>
<menupopup type="select-cat" id="${row}[info_cat]" readonly="true"/>
</menulist>
<vbox cols="1" rows="3" options="0,0,1">
<date-time id="${row}[info_startdate]" readonly="true" options=",8" class="fixedHeight"/>
<date id="${row}[info_enddate]" readonly="true" class="$row_cont[end_class] fixedHeight"/>
<date-time id="${row}[info_datecompleted]" readonly="true" class="fixedHeight"/>
</vbox>
<vbox cols="1" rows="3" options="0,0">
<hbox readonly="true">
<hbox readonly="true" options="1,0">
<date-duration id="${row}[info_used_time]" readonly="true" options="@duration_format"/>
<date-duration id="${row}[info_sum_timesheets]" readonly="true" options="@duration_format" class="timesheet"/>
</hbox>
<description/>
</hbox>
<date-duration id="${row}[info_planned_time]" readonly="true" options="@duration_format" span="all" class="planned"/>
</vbox>
<vbox cols="1" rows="3" options="0,0">
<hbox id="r_used_time" options="1,0">
<image label="Times" src="timesheet"/>
<date-duration id="${row}[info_used_time]" readonly="true" options="@duration_format"/>
<date-duration id="${row}[info_sum_timesheets]" readonly="true" options="@duration_format" class="timesheet"/>
</hbox>
<hbox id="planified" options="1,0">
<image label="planned time" src="k_alarm.png"/>
<date-duration id="${row}[info_planned_time]" readonly="true" options="@duration_format" span="all" class="planned"/>
</hbox>
<hbox id="replanified" options="1,0">
<image label="Re-planned time" src="agt_reload.png"/>
<date-duration id="${row}[info_replanned_time]" readonly="true" options="@duration_format" span="all" class="replanned"/>
</hbox>
</vbox>
<vbox options="0,0">
<menulist>
<menupopup type="select-account" id="${row}[info_owner]" readonly="true"/>
</menulist>
<listbox type="select-account" id="${row}[info_responsible]" readonly="true" rows="5"/>
</vbox>
<vbox options="0" orient="0">
<date-time id="${row}[info_datemodified]" readonly="true"/>
<menulist>
<menupopup type="select-account" id="${row}[info_modifier]" readonly="true"/>
</menulist>
</vbox>
<description value="test" />
<description value="test" />
<description value="test" />
<description value="test" />
<description value="test" />
<description value="test" />
<description value="test" />
<description value="test2" />
</row>
</rows>
</grid>