Implemented Tasks Export (T147, #3861)

Summary: Fixes T147

Reviewers: #roundcube_kolab_plugins_developers

Maniphest Tasks: T147

Differential Revision: https://git.kolab.org/D165
This commit is contained in:
Thomas Bruederli 2016-06-20 10:09:36 +02:00 committed by Jeroen van Meeuwen (Kolab Systems)
parent 98b6513dce
commit 27df671c91
13 changed files with 244 additions and 13 deletions

View file

@ -332,6 +332,10 @@ class tasklist_database_driver extends tasklist_driver
$sql_add .= ' AND changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $filter['since']));
}
if ($filter['uid']) {
$sql_add .= ' AND `uid` IN (' . implode(',', array_map(array($this->rc->db, 'quote'), $filter['uid'])) . ')');
}
$tasks = array();
if (!empty($list_ids)) {
$result = $this->rc->db->query(sprintf(

View file

@ -550,6 +550,7 @@ class tasklist_kolab_driver extends tasklist_driver
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* - uid: Task UIDs
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
@ -585,6 +586,10 @@ class tasklist_kolab_driver extends tasklist_driver
$query[] = array('changed', '>=', $filter['since']);
}
if ($filter['uid']) {
$query[] = array('uid', '=', (array) $filter['uid']);
}
foreach ($lists as $list_id) {
if (!$folder = $this->get_folder($list_id)) {
continue;

View file

@ -13,6 +13,10 @@ $labels['navtitle'] = 'Tasks';
$labels['lists'] = 'Tasklists';
$labels['list'] = 'Tasklist';
$labels['tags'] = 'Tags';
$labels['export'] = 'Export';
$labels['exporttitle'] = 'Export to iCalendar';
$labels['exportattachments'] = 'With attachments';
$labels['currentview'] = 'current view';
$labels['tasklistsubscribe'] = 'List permanently';
$labels['listsearchresults'] = 'Available Tasklists';
$labels['findlists'] = 'Find tasklists...';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -11,7 +11,7 @@
*/
#taskbar a.button-tasklist span.button-inner {
background-image: url(buttons.png);
background-image: url(images/buttons.png);
background-position: 0 0;
}
@ -22,10 +22,22 @@
ul.toolbarmenu li span.icon.taskadd,
#attachmentmenu li a.tasklistlink span.icon.taskadd {
background-image: url(buttons.png);
background-image: url(images/buttons.png);
background-position: -4px -90px;
}
#taskstoolbar a.button.export {
background-image: url(images/buttons.png);
background-position: center -179px;
min-width: 50px;
max-width: 70px;
}
#taskstoolbar a.button.import {
background-image: url(images/buttons.png);
background-position: center -139px;
}
#taskedit.uidialog,
.tasklistview div.uidialog {
display: none;
@ -296,7 +308,7 @@ ul.toolbarmenu li span.icon.taskadd,
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background: url(sprites.png) right 20px no-repeat;
background: url(images/sprites.png) right 20px no-repeat;
}
.quickview-active #tasklistsbox .treelist li input,
@ -341,7 +353,7 @@ ul.toolbarmenu li span.icon.taskadd,
height: 16px;
padding: 0;
margin-right: 4px;
background: url(sprites.png) -200px 0 no-repeat;
background: url(images/sprites.png) -200px 0 no-repeat;
overflow: hidden;
text-indent: -5000px;
cursor: pointer;
@ -461,7 +473,7 @@ ul.toolbarmenu li span.icon.taskadd,
}
#taskstoolbar a.button.newtask {
background-image: url(buttons.png);
background-image: url(images/buttons.png);
background-position: center -53px;
}
@ -547,7 +559,7 @@ ul.toolbarmenu li span.icon.taskadd,
.buttonbar-right a.iconbutton {
padding: 0;
background-image: url(sprites.png);
background-image: url(images/sprites.png);
background-position: 0 -238px;
}
@ -599,7 +611,7 @@ ul.toolbarmenu li span.icon.taskadd,
width: 14px;
height: 14px;
background: url(sprites.png) -2px -80px no-repeat;
background: url(images/sprites.png) -2px -80px no-repeat;
text-indent: -1000px;
overflow: hidden;
}
@ -648,7 +660,7 @@ ul.toolbarmenu li span.icon.taskadd,
display: inline-block;
width: 16px;
height: 16px;
background: url(sprites.png) 1000px -3px no-repeat;
background: url(images/sprites.png) 1000px -3px no-repeat;
margin: -3px 1em 0 0;
vertical-align: middle;
cursor: pointer;
@ -730,7 +742,7 @@ ul.toolbarmenu li span.icon.taskadd,
right: 6px;
width: 18px;
height: 18px;
background: url(sprites.png) 1000px -80px no-repeat;
background: url(images/sprites.png) 1000px -80px no-repeat;
text-indent: -5000px;
overflow: hidden;
cursor: pointer;
@ -772,7 +784,7 @@ ul.toolbarmenu li span.expand,
ul.toolbarmenu li span.collapse,
ul.toolbarmenu li span.history,
ul.toolbarmenu.iconized .selected span.icon {
background-image: url(sprites.png);
background-image: url(images/sprites.png);
}
ul.toolbarmenu li span.add {
@ -1162,7 +1174,7 @@ label.block {
#taskedit-links .attachmentslist li.message.eml,
#task-links .attachmentslist li.message.eml {
background-image: url(sprites.png);
background-image: url(images/sprites.png);
background-position: -2px -388px;
}
@ -1411,7 +1423,7 @@ div.messagetasklinks::before {
left: 8px;
width: 18px;
height: 18px;
background: url(buttons.png) -6px -115px no-repeat;
background: url(images/buttons.png) -6px -115px no-repeat;
}
div.messagetasklinks ul.tasklist {

View file

@ -17,6 +17,7 @@
<div id="taskstoolbar" class="toolbar" role="toolbar" aria-labelledby="aria-label-toolbar">
<roundcube:button command="newtask" type="link" class="button newtask disabled" classAct="button newtask" classSel="button newtask pressed" label="tasklist.newtask" title="tasklist.newtask" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="print" title="tasklist.printtitle" />
<roundcube:button command="export" type="link" class="button export disabled" classAct="button export" classSel="button export pressed" label="tasklist.export" title="tasklist.exporttitle" />
<roundcube:container name="toolbar" id="taskstoolbar" />
</div>
@ -320,6 +321,10 @@
<roundcube:container name="tasklistform" id="tasklistform" />
</div>
<div id="tasksexport" class="uidialog">
<roundcube:object name="plugin.tasks_export_form" id="tasks-export-form" />
</div>
<script type="text/javascript">
// UI startup

View file

@ -820,6 +820,92 @@ function rcube_tasklist_ui(settings)
loadstate.lists = active_lists();
}
// open a tasks export dialog
this.export_tasks = function()
{
// close show dialog first
var $dialog = $("#tasksexport"),
form = rcmail.gui_objects.exportform,
buttons = {};
if (!form)
return;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
$("#task-export-list").val('');
buttons[rcmail.gettext('export', 'tasklist')] = function() {
var source = $('#task-export-list option:selected').val();
// "current view" export, use hidden form to POST task IDs
if (source === '') {
var cache = {}, tasks = [], inputs = [],
postform = $('#tasks-export-form-post');
$.each(listindex || [], function() {
var rec = listdata[this];
if (match_filter(rec, cache)) {
tasks.push(rec.id);
}
});
// copy form inputs, there may be controls added by other plugins
$('#tasksexport select, #tasksexport input').each(function() {
if (this.type != 'checkbox' || this.checked)
inputs.push($('<input>').attr({type: 'hidden', name: this.name, value: this.value}));
});
inputs.push($('<input>').attr({type: 'hidden', name: '_token', value: rcmail.env.request_token}));
inputs.push($('<input>').attr({type: 'hidden', name: 'id', value: tasks.join(',')}));
if (!postform.length)
postform = $('<form>')
.attr({style: 'display: none', method: 'POST', action: '?_task=tasks&_action=export'})
.appendTo('body');
postform.html('').append(inputs).submit();
}
// otherwise we can use simple GET
else {
rcmail.goto_url('export', {source: source, attachments: attach});
}
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'tasklist')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: false,
title: rcmail.gettext('exporttitle', 'tasklist'),
open: function() {
$dialog.parent().find('.ui-dialog-buttonset .ui-button').first().addClass('mainaction');
},
close: function() {
$('.ui-dialog-buttonpane button', $dialog.parent()).button('enable');
$dialog.dialog("destroy").hide();
},
buttons: buttons,
width: 520
}).show();
};
/*
// download the selected task as iCal
this.task_download = function(task)
{
if (task && task.id) {
rcmail.goto_url('export', {source: task.list, id: task.id, attachments: 1});
}
};
*/
/**
* Modify query parameters for refresh requests
*/
@ -3367,6 +3453,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.register_command('list-remove', function(){ rctasks.list_remove(rctasks.selected_list); }, false);
rcmail.register_command('list-showurl', function(){ rctasks.list_showurl(rctasks.selected_list); }, false);
rcmail.register_command('export', function(){ rctasks.export_tasks(); }, true);
rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
rcmail.register_command('expand-all', function(){ rctasks.expand_collapse(true); }, true);

View file

@ -119,6 +119,7 @@ class tasklist extends rcube_plugin
$this->register_action('mail2task', array($this, 'mail_message2task'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->register_action('export', array($this, 'export_tasks'));
$this->register_action('mailimportitip', array($this, 'mail_import_itip'));
$this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
$this->register_action('itip-status', array($this, 'task_itip_status'));
@ -1578,6 +1579,79 @@ class tasklist extends rcube_plugin
return $p;
}
/**
* Construct the ics file for exporting tasks to iCalendar format
*/
function export_tasks()
{
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
$task_id = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC);
$attachments = (bool) rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GPC);
$this->load_driver();
$browser = new rcube_browser;
$lists = $this->driver->get_lists();
$tasks = array();
$filter = array();
// get message UIDs for filter
if ($source && ($list = $lists[$source])) {
$filename = html_entity_decode($list['name']) ?: $sorce;
$filter = array($source => true);
}
else if ($task_id) {
$filename = 'tasks';
foreach (explode(',', $task_id) as $id) {
list($list_id, $task_id) = explode(':', $id, 2);
if ($list_id && $task_id) {
$filter[$list_id][] = $task_id;
}
}
}
// Get tasks
foreach ($filter as $list_id => $uids) {
$_filter = is_array($uids) ? array('uid' => $uids) : null;
$_tasks = $this->driver->list_tasks($_filter, $list_id);
if (!empty($_tasks)) {
$tasks = array_merge($tasks, $_tasks);
}
}
// Set file name
if ($source && count($tasks) == 1) {
$filename = $tasks[0]['title'] ?: 'task';
}
$filename .= '.ics';
$filename = $browser->ie ? rawurlencode($filename) : addcslashes($filename, '"');
$tasks = array_map(array($this, 'to_libcal'), $tasks);
// Give plugins a possibility to implement other output formats or modify the result
$plugin = $this->rc->plugins->exec_hook('tasks_export', array(
'result' => $tasks,
'attachments' => $attachments,
'filename' => $filename,
'plugin' => $this,
));
if ($plugin['abort']) {
exit;
}
$this->rc->output->nocacheing_headers();
// don't kill the connection if download takes more than 30 sec.
@set_time_limit(0);
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=\"". $plugin['filename'] ."\"");
$this->get_ical()->export($plugin['tasks'], '', true,
$plugins['attachments'] ? array($this->driver, 'get_attachment_body') : null);
exit;
}
/******* Attachment handling *******/

View file

@ -161,6 +161,7 @@ class tasklist_ui
$this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
$this->plugin->register_handler('plugin.task_rsvp_buttons', array($this->plugin->itip, 'itip_rsvp_buttons'));
$this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
$this->plugin->register_handler('plugin.tasks_export_form', array($this, 'tasks_export_form'));
jqueryui::tagedit();
@ -310,11 +311,18 @@ class tasklist_ui
*/
function tasklist_select($attrib = array())
{
$attrib['name'] = 'list';
if (empty($attrib['name'])) {
$attrib['name'] = 'list';
}
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
$default = null;
foreach ((array) $attrib['extra'] as $id => $name) {
$select->add($name, $id);
}
foreach ((array)$this->plugin->driver->get_lists() as $id => $prop) {
if ($prop['editable'] || strpos($prop['rights'], 'i') !== false) {
$select->add($prop['name'], $id);
@ -525,6 +533,38 @@ class tasklist_ui
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->plugin->gettext('sendnotifications')));
}
/**
* Form to select options for exporting tasks
*/
function tasks_export_form($attrib = array())
{
if (!$attrib['id']) {
$attrib['id'] = 'rcmTaskExportForm';
}
$html .= html::div('form-section',
html::label('task-export-list', $this->plugin->gettext('list')) .
$this->tasklist_select(array(
'name' => 'source',
'id' => 'task-export-list',
'extra' => array('' => '- ' . $this->plugin->gettext('currentview') . ' -'),
))
);
$checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'task-export-attachments', 'value' => 1));
$html .= html::div('form-section',
html::label('task-export-attachments', $this->plugin->gettext('exportattachments')) .
$checkbox->show(1)
);
$this->register_gui_object('exportform', $attrib['id']);
return html::tag('form', array('action' => $this->rc->url(array('task' => 'tasklist', 'action' => 'export')),
'method' => "post", 'id' => $attrib['id']),
$html
);
}
/**
* Wrapper for rcube_output_html::add_gui_object()
*/

BIN
source/tasklist_buttons.psd Normal file

Binary file not shown.

BIN
source/tasklist_sprites.psd Normal file

Binary file not shown.