Projects
Kolab:3.4
roundcubemail-plugins-kolab
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 20
View file
roundcubemail-plugins-kolab.spec
Changed
@@ -24,7 +24,7 @@ Name: roundcubemail-plugins-kolab Version: 3.1.6 -Release: 1%{?dist} +Release: 2%{?dist} Summary: Kolab Groupware plugins for Roundcube Webmail Group: Applications/Internet @@ -34,6 +34,8 @@ Source0: http://git.kolab.org/%{name}/snapshot/%{name}-%{version}.tar.gz Source1: kolab_logo.png +Patch1: roundcubemail-plugins-kolab-3.1.6-task-enhancements.patch + BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) BuildArch: noarch @@ -49,6 +51,8 @@ %prep %setup -q +%patch1 -p1 + %build %install @@ -208,7 +212,8 @@ %attr(0770,root,%{httpd_group}) %{roundcube_lib}/plugins/odfviewer %changelog -* Thu Oct 31 2013 Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> - 3.1.6-1 +* Thu Oct 31 2013 Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> - 3.1.6-2 +- Also apply patch for task enhancements - New upstream version * Fri Oct 18 2013 Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> - 3.1.5-1
View file
roundcubemail-plugins-kolab-3.1.6-task-enhancements.patch
Added
@@ -0,0 +1,1472 @@ +diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php +index f038caf..685745d 100644 +--- a/plugins/calendar/calendar.php ++++ b/plugins/calendar/calendar.php +@@ -126,6 +126,7 @@ class calendar extends rcube_plugin + $this->register_action('mailtoevent', array($this, 'mail_message2event')); + $this->register_action('inlineui', array($this, 'get_inline_ui')); + $this->register_action('check-recent', array($this, 'check_recent')); ++ $this->add_hook('refresh', array($this, 'refresh')); + + // remove undo information... + if ($undo = $_SESSION'calendar_event_undo') { +@@ -162,6 +163,8 @@ class calendar extends rcube_plugin + 'innerclass' => 'icon calendar', + ))), + 'messagemenu'); ++ ++ $this->api->output->add_label('calendar.createfrommail'); + } + } + +@@ -919,6 +922,38 @@ class calendar extends rcube_plugin + } + + /** ++ * Handler for keep-alive requests ++ * This will check for updated data in active calendars and sync them to the client ++ */ ++ public function refresh($attr) ++ { ++ // refresh the entire calendar every 10th time to also sync deleted events ++ $refetch = rand(0,10) == 10; ++ ++ foreach ($this->driver->list_calendars(true) as $cal) { ++ if ($refetch) { ++ $this->rc->output->command('plugin.refresh_calendar', ++ array('source' => $cal'id', 'refetch' => true)); ++ } ++ else { ++ $events = $this->driver->load_events( ++ get_input_value('start', RCUBE_INPUT_GET), ++ get_input_value('end', RCUBE_INPUT_GET), ++ get_input_value('q', RCUBE_INPUT_GET), ++ $cal'id', ++ 1, ++ $attr'last' ++ ); ++ ++ foreach ($events as $event) { ++ $this->rc->output->command('plugin.refresh_calendar', ++ array('source' => $cal'id', 'update' => $this->_client_event($event))); ++ } ++ } ++ } ++ } ++ ++ /** + * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. + * This will check for pending notifications and pass them to the client + */ +@@ -975,10 +1010,18 @@ class calendar extends rcube_plugin + + if (!$err && $_FILES'_data''tmp_name') { + $calendar = get_input_value('calendar', RCUBE_INPUT_GPC); +- $events = $this->get_ical()->import_from_file($_FILES'_data''tmp_name'); +- +- $count = $errors = 0; + $rangestart = $_REQUEST'_range' ? date_create("now -" . intval($_REQUEST'_range') . " months") : 0; ++ $count = $errors = 0; ++ ++ try { ++ $events = $this->get_ical()->import_from_file($_FILES'_data''tmp_name', 'UTF-8', true); ++ } ++ catch (Exception $e) { ++ $errors = 1; ++ $msg = $e->getMessage(); ++ $events = array(); ++ } ++ + foreach ($events as $event) { + // TODO: correctly handle recurring events which start before $rangestart + if ($event'end' < $rangestart && (!$event'recurrence' || ($event'recurrence''until' && $event'recurrence''until' < $rangestart))) +@@ -1000,8 +1043,9 @@ class calendar extends rcube_plugin + $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice'); + $this->rc->output->command('plugin.import_success', array('source' => $calendar)); + } +- else +- $this->rc->output->command('display_message', $this->gettext('importerror'), 'error'); ++ else { ++ $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : ''))); ++ } + } + else { + if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { +@@ -1012,7 +1056,7 @@ class calendar extends rcube_plugin + $msg = rcube_label('fileuploaderror'); + } + +- $this->rc->output->command('display_message', $msg, 'error'); ++ $this->rc->output->command('plugin.import_error', array('message' => $msg)); + $this->rc->output->command('plugin.unlock_saving', false); + } + +diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js +index c60c89a..33fe9e4 100644 +--- a/plugins/calendar/calendar_base.js ++++ b/plugins/calendar/calendar_base.js +@@ -6,7 +6,7 @@ + * @author Thomas Bruederli <bruederli@kolabsys.com> + * + * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me> +- * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com> ++ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as +@@ -36,10 +36,9 @@ function rcube_calendar(settings) + var me = this; + + // create new event from current mail message +- this.create_from_mail = function() ++ this.create_from_mail = function(uid) + { +- var uid; +- if ((uid = rcmail.get_single_uid())) { ++ if (uid || (uid = rcmail.get_single_uid())) { + // load calendar UI (scripts and edit dialog template) + if (!this.ui_loaded) { + $.when( +@@ -53,7 +52,7 @@ function rcube_calendar(settings) + + me.ui_loaded = true; + me.ui = new rcube_calendar_ui(me.settings); +- me.create_from_mail(); // start over ++ me.create_from_mail(uid); // start over + }); + return; + } +@@ -156,9 +155,6 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { + + // register create-from-mail command to message_commands array + if (rcmail.env.task == 'mail') { +- // place link above 'view source' +- $('#messagemenu a.calendarlink').parent().insertBefore($('#messagemenu a.sourcelink').parent()); +- + rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail() }); + rcmail.addEventListener('plugin.mail2event_dialog', function(p){ cal.mail2event_dialog(p) }); + rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.ui && cal.ui.unlock_saving(); }); +@@ -169,6 +165,15 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { + } + else + rcmail.enable_command('calendar-create-from-mail', true); ++ ++ // add contextmenu item ++ if (window.rcm_contextmenu_register_command) { ++ rcm_contextmenu_register_command( ++ 'calendar-create-from-mail', ++ function(cmd,el){ cal.create_from_mail() }, ++ 'calendar.createfrommail', ++ 'moveto'); ++ } + } + } + +diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js +index 986113f..d5a4308 100644 +--- a/plugins/calendar/calendar_ui.js ++++ b/plugins/calendar/calendar_ui.js +@@ -1981,10 +1981,17 @@ function rcube_calendar_ui(settings) + if (form && form.elements._data.value) { + rcmail.async_upload_form(form, 'import_events', function(e) { + rcmail.set_busy(false, null, me.saving_lock); ++ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable'); ++ ++ // display error message if no sophisticated response from server arrived (e.g. iframe load error) ++ if (me.import_succeeded === null) ++ rcmail.display_message(rcmail.get_label('importerror', 'calendar'), 'error'); + }); + + // display upload indicator ++ me.import_succeeded = null; + me.saving_lock = rcmail.set_busy(true, 'uploading'); ++ $('.ui-dialog-buttonpane button', $dialog.parent()).button('disable'); + } + }; + +@@ -1999,6 +2006,7 @@ function rcube_calendar_ui(settings) + closeOnEscape: false, + title: rcmail.gettext('importevents', 'calendar'), + close: function() { ++ $('.ui-dialog-buttonpane button', $dialog.parent()).button('enable'); + $dialog.dialog("destroy").hide(); + }, + buttons: buttons, +@@ -2010,6 +2018,7 @@ function rcube_calendar_ui(settings) + // callback from server if import succeeded + this.import_success = function(p) + { ++ this.import_succeeded = true; + $("#eventsimport:ui-dialog").dialog('close'); + rcmail.set_busy(false, null, me.saving_lock); + rcmail.gui_objects.importform.reset(); +@@ -2018,6 +2027,13 @@ function rcube_calendar_ui(settings) + this.refresh(p); + }; + ++ // callback from server to report errors on import ++ this.import_error = function(p) ++ { ++ this.import_succeeded = false; ++ rcmail.display_message(p.message || rcmail.get_label('importerror', 'calendar'), 'error'); ++ } ++ + // show URL of the given calendar in a dialog box + this.showurl = function(calendar) + { +@@ -2087,6 +2103,20 @@ function rcube_calendar_ui(settings) + fc.fullCalendar('removeEvents', function(e){ return e.temp; }); + }; + ++ // modify query parameters for refresh requests ++ this.before_refresh = function(query) ++ { ++ var view = fc.fullCalendar('getView'); ++ ++ query.start = date2unixtime(view.visStart); ++ query.end = date2unixtime(view.visEnd); ++ ++ if (this.search_query) ++ query.q = this.search_query; ++ ++ return query; ++ }; ++ + + /*** event searching ***/ + +@@ -2780,6 +2810,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { + rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); }); + rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); }); + rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); }); ++ rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); }); ++ rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); }); + + // let's go + var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings)); +diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php +index 52de901..c09d8b9 100644 +--- a/plugins/calendar/drivers/calendar_driver.php ++++ b/plugins/calendar/drivers/calendar_driver.php +@@ -236,9 +236,11 @@ abstract class calendar_driver + * @param integer Event's new end (unix timestamp) + * @param string Search query (optional) + * @param mixed List of calendar IDs to load events from (either as array or comma-separated string) ++ * @param boolean Include virtual/recurring events (optional) ++ * @param integer Only list events modified since this time (unix timestamp) + * @return array A list of event objects (see header of this file for struct of an event) + */ +- abstract function load_events($start, $end, $query = null, $calendars = null); ++ abstract function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null); + + /** + * Get a list of pending alarms to be displayed to the user +diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php +index 8cd363c..a2cb903 100644 +--- a/plugins/calendar/drivers/database/database_driver.php ++++ b/plugins/calendar/drivers/database/database_driver.php +@@ -724,7 +724,7 @@ class database_driver extends calendar_driver + * + * @see calendar_driver::load_events() + */ +- public function load_events($start, $end, $query = null, $calendars = null) ++ public function load_events($start, $end, $query = null, $calendars = null, $virtual = 1, $modifiedsince = null) + { + if (empty($calendars)) + $calendars = array_keys($this->calendars); +@@ -742,6 +742,12 @@ class database_driver extends calendar_driver + $sql_add = 'AND (' . join(' OR ', $sql_query) . ')'; + } + ++ if (!$virtual) ++ $sql_arr .= ' AND e.recurrence_id = 0'; ++ ++ if ($modifiedsince) ++ $sql_add .= ' AND e.changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $modifiedsince)); ++ + $events = array(); + if (!empty($calendar_ids)) { + $result = $this->rc->db->query(sprintf( +diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php +index b8171b7..0722d23 100644 +--- a/plugins/calendar/drivers/kolab/kolab_driver.php ++++ b/plugins/calendar/drivers/kolab/kolab_driver.php +@@ -733,20 +733,25 @@ class kolab_driver extends calendar_driver + * @param integer Event's new end (unix timestamp) + * @param string Search query (optional) + * @param mixed List of calendar IDs to load events from (either as array or comma-separated string) +- * @param boolean Strip virtual events (optional) ++ * @param boolean Include virtual events (optional) ++ * @param integer Only list events modified since this time (unix timestamp) + * @return array A list of event records + */ +- public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1) ++ public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null) + { + if ($calendars && is_string($calendars)) + $calendars = explode(',', $calendars); + ++ $query = array(); ++ if ($modifiedsince) ++ $query = array('changed', '>=', $modifiedsince); ++ + $events = $categories = array(); + foreach (array_keys($this->calendars) as $cid) { + if ($calendars && !in_array($cid, $calendars)) + continue; + +- $events = array_merge($events, $this->calendars$cid->list_events($start, $end, $search, $virtual)); ++ $events = array_merge($events, $this->calendars$cid->list_events($start, $end, $search, $virtual, $query)); + $categories += $this->calendars$cid->categories; + } + +diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php +index 8ad776a..3c5ad38 100644 +--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php ++++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php +@@ -286,7 +286,7 @@ class tasklist_database_driver extends tasklist_driver + + if ($filter'mask' & tasklist::FILTER_MASK_COMPLETE) + $sql_add .= ' AND complete=1'; +- else // don't show complete tasks by default ++ else if (empty($filter'since')) // don't show complete tasks by default + $sql_add .= ' AND complete<1'; + + if ($filter'mask' & tasklist::FILTER_MASK_FLAGGED) +@@ -301,6 +301,10 @@ class tasklist_database_driver extends tasklist_driver + $sql_add = 'AND (' . join(' OR ', $sql_query) . ')'; + } + ++ if ($filter'since' && is_numeric($filter'since')) { ++ $sql_add .= ' AND changed >= ' . $this->rc->db->quote(date('Y-m-d H:i:s', $filter'since')); ++ } ++ + $tasks = array(); + if (!empty($list_ids)) { + $result = $this->rc->db->query(sprintf( +diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +index c630a41..fd3e5c3 100644 +--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php ++++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +@@ -371,7 +371,7 @@ class tasklist_kolab_driver extends tasklist_driver + $query = array(); + if ($filter'mask' & tasklist::FILTER_MASK_COMPLETE) + $query = array('tags','~','x-complete'); +- else ++ else if (empty($filter'since')) + $query = array('tags','!~','x-complete'); + + // full text search (only works with cache enabled) +@@ -382,6 +382,10 @@ class tasklist_kolab_driver extends tasklist_driver + } + } + ++ if ($filter'since') { ++ $query = array('changed', '>=', $filter'since'); ++ } ++ + foreach ($lists as $list_id) { + $folder = $this->folders$list_id; + foreach ((array)$folder->select($query) as $record) { +diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc +index 57d6c3c..ba331be 100644 +--- a/plugins/tasklist/localization/en_US.inc ++++ b/plugins/tasklist/localization/en_US.inc +@@ -51,6 +51,8 @@ $labels'listactions' = 'List options...'; + $labels'listname' = 'Name'; + $labels'showalarms' = 'Show alarms'; + $labels'import' = 'Import'; ++$labels'viewoptions' = 'View options'; ++$labels'focusview' = 'View only this list'; + + // date words + $labels'on' = 'on'; +diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png +index 5224f6f..5c6b9fd 100644 +Binary files a/plugins/tasklist/skins/larry/sprites.png and b/plugins/tasklist/skins/larry/sprites.png differ +diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css +index 173704d..7a1df99 100644 +--- a/plugins/tasklist/skins/larry/tasklist.css ++++ b/plugins/tasklist/skins/larry/tasklist.css +@@ -66,7 +66,7 @@ body.attachmentwin #topnav .topright { + } + + #taskselector { +- margin: -4px 0 0; ++ margin: -4px 40px 0 0; + padding: 0; + } + +@@ -180,16 +180,46 @@ body.attachmentwin #topnav .topright { + #tagslist li { + display: inline-block; + color: #004458; +- margin-right: 0.5em; ++ padding-right: 0.2em; ++ margin-right: 0.3em; + margin-bottom: 0.4em; + min-width: 1.2em; + cursor: pointer; + } + ++#tagslist li.inactive { ++ color: #89b3be; ++ padding-right: 0.6em; ++ font-size: 80%; ++/* display: none; */ ++} ++ ++#tagslist li .count { ++ position: relative; ++ top: -1px; ++ margin-left: 5px; ++ padding: 0.15em 0.5em; ++ font-size: 80%; ++ font-weight: bold; ++ color: #59838e; ++ background: #c7e3ef; ++ box-shadow: inset 0 1px 1px 0 #b0ccd7; ++ -o-box-shadow: inset 0 1px 1px 0 #b0ccd7; ++ -webkit-box-shadow: inset 0 1px 1px 0 #b0ccd7; ++ -moz-box-shadow: inset 0 1px 1px 0 #b0ccd7; ++ border-color: #b0ccd7; ++ border-radius: 8px; ++} ++ ++.tag-draghelper .tag .count, ++#tagslist li.inactive .count { ++ display: none; ++} ++ + #tasklists li { + margin: 0; + height: 20px; +- padding: 6px 8px 2px; ++ padding: 6px 8px 2px 6px; + display: block; + position: relative; + white-space: nowrap; +@@ -206,10 +236,13 @@ body.attachmentwin #topnav .topright { + + #tasklists li span.listname { + display: block; ++ position: absolute; ++ top: 7px; ++ left: 26px; ++ right: 26px; + cursor: default; + padding-bottom: 2px; + padding-right: 30px; +- margin-right: 20px; + color: #004458; + overflow: hidden; + text-overflow: ellipsis; +@@ -218,7 +251,20 @@ body.attachmentwin #topnav .topright { + } + + #tasklists li span.handle { +- display: none; ++ display: inline-block; ++ width: 16px; ++ height: 16px; ++ margin-right: 4px; ++ background: url(sprites.png) -200px 0 no-repeat; ++ cursor: pointer; ++} ++ ++#tasklists li:hover span.handle { ++ background-position: -20px -101px; ++} ++ ++#tasklists li.focusview span.handle { ++ background-position: -2px -101px; + } + + #tasklists li.selected span.listname { +@@ -249,6 +295,11 @@ body.attachmentwin #topnav .topright { + color: #aaa; + } + ++#tasklists li.virtual span.handle { ++ background: none; ++ cursor: default; ++} ++ + #tasklists li input { + position: absolute; + top: 5px; +@@ -336,6 +387,27 @@ body.attachmentwin #topnav .topright { + background: -ms-linear-gradient(top, #eee 0%, #dfdfdf 100%); + background: linear-gradient(top, #eee 0%, #dfdfdf 100%); + border-bottom: 1px solid #ccc; ++ position: relative; ++} ++ ++#tasksview .buttonbar .buttonbar-right { ++ position: absolute; ++ top: 6px; ++ right: 8px; ++} ++ ++.buttonbar-right .listmenu { ++ display: inline-block; ++ cursor: pointer; ++} ++ ++.buttonbar-right .listmenu .inner { ++ display: inline-block; ++ height: 18px; ++ width: 20px; ++ padding: 0; ++ background: url(sprites.png) 0 -237px no-repeat; ++ text-indent: -5000px; + } + + #thelist { +@@ -456,6 +528,7 @@ body.attachmentwin #topnav .topright { + text-align: right; + } + ++.tag-draghelper .tag, + .taskhead .tags .tag { + font-size: 85%; + background: #d9ecf4; +@@ -465,6 +538,11 @@ body.attachmentwin #topnav .topright { + margin-right: 3px; + } + ++.tag-draghelper li.tag { ++ list-style: none; ++ font-size: 100%; ++} ++ + .taskhead .date { + position: absolute; + top: 4px; +@@ -547,9 +625,22 @@ body.attachmentwin #topnav .topright { + border-top: 1px solid #219de6; + } + +-ul.toolbarmenu li span.add { ++ul.toolbarmenu li span.add, ++ul.toolbarmenu li span.expand, ++ul.toolbarmenu li span.collapse { + background-image: url(sprites.png); +- background-position: 0 -100px; ++} ++ ++ul.toolbarmenu li span.add { ++ background-position: 0 -302px; ++} ++ ++ul.toolbarmenu li span.expand { ++ background-position: 0 -258px; ++} ++ ++ul.toolbarmenu li span.collapse { ++ background-position: 0 -280px; + } + + ul.toolbarmenu li span.delete { +@@ -806,6 +897,12 @@ label.block { + /* cursor: pointer; */ + } + ++.form-section span.tag-element.inherit { ++ color: #666; ++ background: #f2f2f2; ++ border-color: #ddd; ++} ++ + .tagedit-list li.tagedit-listelement-old a.tagedit-close, + .tagedit-list li.tagedit-listelement-old a.tagedit-break, + .tagedit-list li.tagedit-listelement-old a.tagedit-delete, +diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html +index 9196d02..52463e0 100644 +--- a/plugins/tasklist/skins/larry/templates/mainview.html ++++ b/plugins/tasklist/skins/larry/templates/mainview.html +@@ -59,6 +59,10 @@ + <li class="nodate"><a href="#nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li> + <li class="complete"><a href="#complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li> + </ul> ++ ++ <div class="buttonbar-right"> ++ <roundcube:button name="taskviewmenulink" id="taskviewmenulink" type="link" title="tasklist.viewoptions" class="listmenu viewoptions" onclick="UI.show_popup('taskviewmenu');return false" innerClass="inner" content="⚙" /> ++ </div> + </div> + + <div class="scroller"> +@@ -92,6 +96,13 @@ + </ul> + </div> + ++<div id="taskviewmenu" class="popupmenu"> ++ <ul class="toolbarmenu"> ++ <li><roundcube:button command="expand-all" label="expand-all" class="icon" classAct="icon active" innerclass="icon expand" /></li> ++ <li><roundcube:button command="collapse-all" label="collapse-all" class="icon" classAct="icon active" innerclass="icon collapse" /></li> ++ </ul> ++</div> ++ + <div id="taskshow"> + <div class="form-section" id="task-parent-title"></div> + <div class="form-section"> +@@ -146,6 +157,8 @@ $(document).ready(function(e){ + UI.init(); + new rcube_splitter({ id:'taskviewsplitter', p1:'#sidebar', p2:'#mainview-right', + orientation:'v', relative:true, start:240, min:180, size:16, offset:2 }).init(); ++ new rcube_splitter({ id:'taskviewsplitterv', p1:'#tagsbox', p2:'#tasklistsbox', ++ orientation:'h', relative:true, start:242, min:120, size:16, offset:6 }).init(); + }); + + </script> +diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js +index d76c9d1..25ab68d 100644 +--- a/plugins/tasklist/tasklist.js ++++ b/plugins/tasklist/tasklist.js +@@ -54,6 +54,7 @@ function rcube_tasklist_ui(settings) + var filtermask = FILTER_MASK_ALL; + var loadstate = { filter:-1, lists:'', search:null }; + var idcount = 0; ++ var focusview; + var saving_lock; + var ui_loading; + var taskcounts = {}; +@@ -64,6 +65,8 @@ function rcube_tasklist_ui(settings) + var search_request; + var search_query; + var completeness_slider; ++ var task_draghelper; ++ var tag_draghelper; + var me = this; + + // general datepicker settings +@@ -92,6 +95,7 @@ function rcube_tasklist_ui(settings) + this.add_childtask = add_childtask; + this.quicksearch = quicksearch; + this.reset_search = reset_search; ++ this.expand_collapse = expand_collapse; + this.list_remove = list_remove; + this.list_edit_dialog = list_edit_dialog; + this.unlock_saving = unlock_saving; +@@ -116,22 +120,30 @@ function rcube_tasklist_ui(settings) + init_tasklist_li(li, id); + } + +- if (me.tasklistsid.editable && !me.selected_list) { ++ if (me.tasklistsid.editable && (!me.selected_list || (me.tasklistsid.active && !me.tasklistsme.selected_list.active))) { + me.selected_list = id; +- rcmail.enable_command('addtask', true); +- $(li).click(); + } + } + ++ if (me.selected_list) { ++ rcmail.enable_command('addtask', true); ++ $(rcmail.get_folder_li(me.selected_list, 'rcmlitasklist')).click(); ++ } ++ + // register server callbacks + rcmail.addEventListener('plugin.data_ready', data_ready); +- rcmail.addEventListener('plugin.refresh_task', update_taskitem); ++ rcmail.addEventListener('plugin.update_task', update_taskitem); ++ rcmail.addEventListener('plugin.refresh_tasks', function(p) { update_taskitem(p, true); }); + rcmail.addEventListener('plugin.update_counts', update_counts); + rcmail.addEventListener('plugin.insert_tasklist', insert_list); + rcmail.addEventListener('plugin.update_tasklist', update_list); + rcmail.addEventListener('plugin.destroy_tasklist', destroy_list); +- rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null); }); + rcmail.addEventListener('plugin.unlock_saving', unlock_saving); ++ rcmail.addEventListener('requestrefresh', before_refresh); ++ rcmail.addEventListener('plugin.reload_data', function(){ ++ list_tasks(null, true); ++ setTimeout(fetch_counts, 200); ++ }); + + // start loading tasks + fetch_counts(); +@@ -335,7 +347,8 @@ function rcube_tasklist_ui(settings) + $(input).datepicker('widget').find('button.ui-datepicker-close') + .html(rcmail.gettext('nodate','tasklist')) + .attr('onclick', '') +- .click(function(e){ ++ .unbind('click') ++ .bind('click', function(e){ + $(input).datepicker('setDate', null).datepicker('hide'); + }); + }, 1); +@@ -393,7 +406,7 @@ function rcube_tasklist_ui(settings) + /** + * List tasks matching the given selector + */ +- function list_tasks(sel) ++ function list_tasks(sel, force) + { + if (rcmail.busy) + return; +@@ -405,7 +418,7 @@ function rcube_tasklist_ui(settings) + + var active = active_lists(), + basefilter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL, +- reload = active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query; ++ reload = force || active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query; + + if (active.length && reload) { + ui_loading = rcmail.set_busy(true, 'loading'); +@@ -439,6 +452,19 @@ function rcube_tasklist_ui(settings) + } + + /** ++ * Modify query parameters for refresh requests ++ */ ++ function before_refresh(query) ++ { ++ query.filter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL; ++ query.lists = active_lists().join(','); ++ if (search_query) ++ query.q = search_query; ++ ++ return query; ++ } ++ ++ /** + * Callback if task data from server is ready + */ + function data_ready(response) +@@ -459,8 +485,9 @@ function rcube_tasklist_ui(settings) + listdatalistdataid.parent_id.children.push(id); + } + +- render_tasklist(); + append_tags(response.tags || ); ++ render_tasklist(); ++ + rcmail.set_busy(false, 'loading', ui_loading); + } + +@@ -473,6 +500,7 @@ function rcube_tasklist_ui(settings) + var id, rec, + count = 0, + cache = {}, ++ activetags = {}, + msgbox = $('#listmessagebox').hide(), + list = $(rcmail.gui_objects.resultlist).html(''); + +@@ -482,10 +510,19 @@ function rcube_tasklist_ui(settings) + if (match_filter(rec, cache)) { + render_task(rec); + count++; ++ ++ // keep a list of tags from all visible tasks ++ for (var t, j=0; rec.tags && j < rec.tags.length; j++) { ++ t = rec.tagsj; ++ if (typeof activetagst == 'undefined') ++ activetagst = 0; ++ activetagst++; ++ } + } + } + + fix_tree_toggles(); ++ update_tagcloud(activetags); + + if (!count) + msgbox.html(rcmail.gettext('notasksfound','tasklist')).show(); +@@ -506,6 +543,30 @@ function rcube_tasklist_ui(settings) + } + + /** ++ * Expand/collapse all task items with childs ++ */ ++ function expand_collapse(expand) ++ { ++ var collapsed = !expand; ++ ++ $('.taskitem .childtasks')(collapsed ? 'hide' : 'show')(); ++ $('.taskitem .childtoggle') ++ .removeClass(collapsed ? 'expanded' : 'collapsed') ++ .addClass(collapsed ? 'collapsed' : 'expanded') ++ .html(collapsed ? '▶' : '▼'); ++ ++ // store new toggle collapse states ++ var ids = ; ++ for (var id in listdata) { ++ if (listdataid.children && listdataid.children.length) ++ ids.push(id); ++ } ++ if (ids.length) { ++ rcmail.http_post('tasks/task', { action:'collapse', t:{ id:ids.join(',') }, collapsed:collapsed?1:0 }); ++ } ++ } ++ ++ /** + * + */ + function append_tags(taglist) +@@ -520,8 +581,19 @@ function rcube_tasklist_ui(settings) + + // append new tags to tag cloud + $.each(newtags, function(i, tag){ +- $('<li>').attr('rel', tag).data('value', tag).html(Q(tag)).appendTo(rcmail.gui_objects.tagslist); +- }); ++ $('<li>').attr('rel', tag).data('value', tag) ++ .html(Q(tag) + '<span class="count"></span>') ++ .appendTo(rcmail.gui_objects.tagslist) ++ .draggable({ ++ addClasses: false, ++ revert: 'invalid', ++ revertDuration: 300, ++ helper: tag_draggable_helper, ++ start: tag_draggable_start, ++ appendTo: 'body', ++ cursor: 'pointer' ++ }); ++ }); + + // re-sort tags list + $(rcmail.gui_objects.tagslist).children('li').sortElements(function(a,b){ +@@ -530,6 +602,90 @@ function rcube_tasklist_ui(settings) + } + + /** ++ * Display the given counts to each tag and set those inactive which don't ++ * have any matching tasks in the current view. ++ */ ++ function update_tagcloud(counts) ++ { ++ // compute counts first by iterating over all visible task items ++ if (typeof counts == 'undefined') { ++ counts = {}; ++ $('li.taskitem', rcmail.gui_objects.resultlist).each(function(i,li){ ++ var t, id = $(li).attr('rel'), ++ rec = listdataid; ++ for (var j=0; rec && rec.tags && j < rec.tags.length; j++) { ++ t = rec.tagsj; ++ if (typeof countst == 'undefined') ++ countst = 0; ++ countst++; ++ } ++ }); ++ } ++ ++ $(rcmail.gui_objects.tagslist).children('li').each(function(i,li){ ++ var elem = $(li), tag = elem.attr('rel'), ++ count = countstag || 0; ++ ++ elem.children('.count').html(count+''); ++ if (count == 0) elem.addClass('inactive'); ++ else elem.removeClass('inactive'); ++ }); ++ } ++ ++ /* Helper functions for drag & drop functionality of tags */ ++ ++ function tag_draggable_helper() ++ { ++ if (!tag_draghelper) ++ tag_draghelper = $('<div class="tag-draghelper"></div>'); ++ else ++ tag_draghelper.html(''); ++ ++ $(this).clone().addClass('tag').appendTo(tag_draghelper); ++ return tag_draghelper; ++ } ++ ++ function tag_draggable_start(event, ui) ++ { ++ $('.taskhead').droppable({ ++ hoverClass: 'droptarget', ++ accept: tag_droppable_accept, ++ drop: tag_draggable_dropped, ++ addClasses: false ++ }); ++ } ++ ++ function tag_droppable_accept(draggable) ++ { ++ if (rcmail.busy) ++ return false; ++ ++ var tag = draggable.data('value'), ++ drop_id = $(this).data('id'), ++ drop_rec = listdatadrop_id; ++ ++ // target already has this tag assigned ++ if (!drop_rec || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) { ++ return false; ++ } ++ ++ return true; ++ } ++ ++ function tag_draggable_dropped(event, ui) ++ { ++ var drop_id = $(this).data('id'), ++ tag = ui.draggable.data('value'), ++ rec = listdatadrop_id; ++ ++ if (rec && rec.id) { ++ if (!rec.tags) rec.tags = ; ++ rec.tags.push(tag); ++ save_task(rec, 'edit'); ++ } ++ } ++ ++ /** + * + */ + function update_counts(counts) +@@ -553,11 +709,11 @@ function rcube_tasklist_ui(settings) + /** + * Callback from server to update a single task item + */ +- function update_taskitem(rec) ++ function update_taskitem(rec, filter) + { + // handle a list of task records + if ($.isArray(rec)) { +- $.each(rec, function(i,r){ update_taskitem(r); }); ++ $.each(rec, function(i,r){ update_taskitem(r, filter); }); + return; + } + +@@ -597,12 +753,16 @@ function rcube_tasklist_ui(settings) + } + } + +- if (list.active) +- render_task(rec, oldid); +- else ++ if (list.active || rec.tempid) { ++ if (!filter || match_filter(rec, {})) ++ render_task(rec, oldid); ++ } ++ else { + $('lirel="'+id+'"', rcmail.gui_objects.resultlist).remove(); ++ } + + append_tags(rec.tags || ); ++ update_tagcloud(); + fix_tree_toggles(); + } + +@@ -655,10 +815,10 @@ function rcube_tasklist_ui(settings) + revert: 'invalid', + addClasses: false, + cursorAt: { left:-10, top:12 }, +- helper: draggable_helper, ++ helper: task_draggable_helper, + appendTo: 'body', +- start: draggable_start, +- stop: draggable_stop, ++ start: task_draggable_start, ++ stop: task_draggable_stop, + revertDuration: 300 + }); + +@@ -703,7 +863,7 @@ function rcube_tasklist_ui(settings) + */ + function resort_task(rec, li, animated) + { +- var dir = 0, index, slice, next_li, next_id, next_rec; ++ var dir = 0, index, slice, cmp, next_li, next_id, next_rec, insert_after, past_myself; + + // animated moving + var insert_animated = function(li, before, after) { +@@ -729,33 +889,36 @@ function rcube_tasklist_ui(settings) + } + + // find the right place to insert the task item +- li.siblings().each(function(i, elem){ ++ li.parent().children('.taskitem').each(function(i, elem){ + next_li = $(elem); + next_id = next_li.attr('rel'); + next_rec = listdatanext_id; + + if (next_id == rec.id) { +- next_li = null; ++ past_myself = true; + return 1; // continue + } + +- if (next_rec && task_cmp(rec, next_rec) > 0) { ++ cmp = next_rec ? task_cmp(rec, next_rec) : 0; ++ ++ if (cmp > 0 || (cmp == 0 && !past_myself)) { ++ insert_after = next_li; + return 1; // continue; + } +- else if (next_rec && next_li && task_cmp(rec, next_rec) < 0) { ++ else if (next_li && cmp < 0) { + if (animated) insert_animated(li, next_li); + else li.insertBefore(next_li); +- next_li = null; +- return false; ++ index = $.inArray(next_id, listindex); ++ return false; // break + } + }); + +- index = $.inArray(next_id, listindex); ++ if (insert_after) { ++ if (animated) insert_animated(li, null, insert_after); ++ else li.insertAfter(insert_after); + +- if (next_li) { +- if (animated) insert_animated(li, null, next_li); +- else li.insertAfter(next_li); +- index++; ++ next_id = insert_after.attr('rel'); ++ index = $.inArray(next_id, listindex); + } + + // insert into list index +@@ -799,20 +962,20 @@ function rcube_tasklist_ui(settings) + + /* Helper functions for drag & drop functionality */ + +- function draggable_helper() ++ function task_draggable_helper() + { +- if (!draghelper) +- draghelper = $('<div class="taskitem-draghelper">✔</div>'); ++ if (!task_draghelper) ++ task_draghelper = $('<div class="taskitem-draghelper">✔</div>'); + +- return draghelper; ++ return task_draghelper; + } + +- function draggable_start(event, ui) ++ function task_draggable_start(event, ui) + { + $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.folderlist.id+' li').droppable({ + hoverClass: 'droptarget', +- accept: droppable_accept, +- drop: draggable_dropped, ++ accept: task_droppable_accept, ++ drop: task_draggable_dropped, + addClasses: false + }); + +@@ -820,13 +983,13 @@ function rcube_tasklist_ui(settings) + $('#rootdroppable').show(); + } + +- function draggable_stop(event, ui) ++ function task_draggable_stop(event, ui) + { + $(this).parent().removeClass('dragging'); + $('#rootdroppable').hide(); + } + +- function droppable_accept(draggable) ++ function task_droppable_accept(draggable) + { + if (rcmail.busy) + return false; +@@ -858,7 +1021,7 @@ function rcube_tasklist_ui(settings) + return true; + } + +- function draggable_dropped(event, ui) ++ function task_draggable_dropped(event, ui) + { + var drop_id = $(this).data('id'), + task_id = ui.draggable.data('id'), +@@ -921,10 +1084,23 @@ function rcube_tasklist_ui(settings) + $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%'); + $('#task-list .task-text').html(Q(me.tasklistsrec.list ? me.tasklistsrec.list.name : '')); + +- var taglist = $('#task-tags')(rec.tags && rec.tags.length ? 'show' : 'hide')().children('.task-text').empty(); ++ var itags = get_inherited_tags(rec); ++ var taglist = $('#task-tags')(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')().children('.task-text').empty(); + if (rec.tags && rec.tags.length) { + $.each(rec.tags, function(i,val){ +- $('<span>').addClass('tag-element').html(Q(val)).data('value', val).appendTo(taglist); ++ $('<span>').addClass('tag-element').html(Q(val)).appendTo(taglist); ++ }); ++ } ++ ++ // append inherited tags ++ if (itags.length) { ++ $.each(itags, function(i,val){ ++ if (!rec.tags || $.inArray(val, rec.tags) < 0) ++ $('<span>').addClass('tag-element inherit').html(Q(val)).appendTo(taglist); ++ }); ++ // re-sort tags list ++ $(taglist).children().sortElements(function(a,b){ ++ return $.text(a).toLowerCase() > $.text(b).toLowerCase() ? 1 : -1; + }); + } + +@@ -1012,7 +1188,7 @@ function rcube_tasklist_ui(settings) + var recstarttime = $('#taskedit-starttime').val(rec.starttime || ''); + var complete = $('#taskedit-completeness').val((rec.complete || 0) * 100); + completeness_slider.slider('value', complete.val()); +- var tasklist = $('#taskedit-tasklist').val(rec.list || 0).prop('disabled', rec.parent_id ? true : false); ++ var tasklist = $('#taskedit-tasklist').val(rec.list || me.selected_list).prop('disabled', rec.parent_id ? true : false); + + // tag-edit line + var tagline = $(rcmail.gui_objects.edittagline).empty(); +@@ -1102,10 +1278,16 @@ function rcube_tasklist_ui(settings) + } + } + ++ // collect tags + $('inputtype="hidden"', rcmail.gui_objects.edittagline).each(function(i,elem){ + if (elem.value) + me.selected_task.tags.push(elem.value); + }); ++ // including the "pending" one in the text box ++ var newtag = $('#tagedit-input').val(); ++ if (newtag != '') { ++ me.selected_task.tags.push(newtag); ++ } + + // serialize alarm settings + var alarm = $('#taskedit select.edit-alarm-type').val(); +@@ -1124,7 +1306,7 @@ function rcube_tasklist_ui(settings) + } + + // task assigned to a new list +- if (me.selected_task.list && me.selected_task.list != rec.list) { ++ if (me.selected_task.list && listdataid && me.selected_task.list != listdataid.list) { + me.selected_task._fromlist = rec.list; + } + +@@ -1135,6 +1317,9 @@ function rcube_tasklist_ui(settings) + if (!me.selected_task.list && list.id) + me.selected_task.list = list.id; + ++ if (!me.selected_task.tags.length) ++ me.selected_task.tags = ''; ++ + if (save_task(me.selected_task, action)) + $dialog.dialog('close'); + }; +@@ -1367,12 +1552,17 @@ function rcube_tasklist_ui(settings) + return cacherec.id; + } + +- var match = !filtermask || (filtermask & rec.mask) > 0 ++ var match = !filtermask || (filtermask & rec.mask) > 0; ++ ++ // in focusview mode, only tasks from the selected list are allowed ++ if (focusview && rec.list != focusview) ++ match = false; + + if (match && tagsfilter.length) { + match = rec.tags && rec.tags.length; ++ var alltags = get_inherited_tags(rec).concat(rec.tags || ); + for (var i=0; match && i < tagsfilter.length; i++) { +- if ($.inArray(tagsfilteri, rec.tags) < 0) ++ if ($.inArray(tagsfilteri, alltags) < 0) + match = false; + } + } +@@ -1402,6 +1592,23 @@ function rcube_tasklist_ui(settings) + /** + * + */ ++ function get_inherited_tags(rec) ++ { ++ var parent_id, itags = ; ++ ++ if ((parent_id = rec.parent_id)) { ++ while (parent_id && listdataparent_id) { ++ itags = itags.concat(listdataparent_id.tags || ); ++ parent_id = listdataparent_id.parent_id; ++ } ++ } ++ ++ return $.unqiqueStrings(itags); ++ } ++ ++ /** ++ * ++ */ + function list_edit_dialog(id) + { + var list = me.tasklistsid, +@@ -1671,6 +1878,11 @@ function rcube_tasklist_ui(settings) + if (!this.checked) remove_tasks(id); + else list_tasks(null); + rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:me.tasklistsid.active?1:0 } }); ++ ++ // disable focusview ++ if (!this.checked && focusview == id) { ++ set_focusview(null); ++ } + } + }).data('id', id).get(0).checked = me.tasklistsid.active || false; + +@@ -1679,6 +1891,15 @@ function rcube_tasklist_ui(settings) + rcmail.select_folder(id, 'rcmlitasklist'); + rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklistsid.editable); + me.selected_list = id; ++ ++ // click on handle icon toggles focusview ++ if (e.target.className == 'handle') { ++ set_focusview(focusview == id ? null : id) ++ } ++ // disable focusview when selecting another list ++ else if (focusview && id != focusview) { ++ set_focusview(null); ++ } + }) + .dblclick(function(e){ + list_edit_dialog($(this).data('id')); +@@ -1688,6 +1909,31 @@ function rcube_tasklist_ui(settings) + .addClass(me.tasklistsid.editable ? null : 'readonly'); + } + ++ /** ++ * Enable/disable focusview mode for the given list ++ */ ++ function set_focusview(id) ++ { ++ if (focusview && focusview != id) ++ $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).removeClass('focusview'); ++ ++ focusview = id; ++ ++ // activate list if necessary ++ if (focusview && !me.tasklistsid.active) { ++ $('input', rcmail.get_folder_li(id, 'rcmlitasklist')).get(0).checked = true; ++ me.tasklistsid.active = true; ++ fetch_counts(); ++ } ++ ++ // update list ++ list_tasks(null); ++ ++ if (focusview) { ++ $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).addClass('focusview'); ++ } ++ } ++ + + // init dialog by default + init_taskedit(); +@@ -1714,6 +1960,22 @@ jQuery.fn.sortElements = (function(){ + }; + })(); + ++// equivalent to $.unique() but working on arrays of strings ++jQuery.unqiqueStrings = (function() { ++ return function(arr) { ++ var hash = {}, out = ; ++ ++ for (var i = 0; i < arr.length; i++) { ++ hasharri = 0; ++ } ++ for (var val in hash) { ++ out.push(val); ++ } ++ ++ return out; ++ }; ++})(); ++ + + /* tasklist plugin UI initialization */ + var rctasks; +@@ -1731,6 +1993,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { + + 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); ++ rcmail.register_command('collapse-all', function(){ rctasks.expand_collapse(false); }, true); + + rctasks.init(); + }); +diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php +index 6ebddc4..e77bccc 100644 +--- a/plugins/tasklist/tasklist.php ++++ b/plugins/tasklist/tasklist.php +@@ -90,6 +90,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->add_hook('refresh', array($this, 'refresh')); + + $this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', ''))); + } +@@ -112,6 +113,8 @@ class tasklist extends rcube_plugin + 'innerclass' => 'icon taskadd', + ))), + 'messagemenu'); ++ ++ $this->api->output->add_label('tasklist.createfrommail'); + } + } + +@@ -247,13 +250,15 @@ class tasklist extends rcube_plugin + break; + + case 'collapse': +- if (intval(get_input_value('collapsed', RCUBE_INPUT_GPC))) { +- $this->collapsed_tasks = $rec'id'; +- } +- else { +- $i = array_search($rec'id', $this->collapsed_tasks); +- if ($i !== false) +- unset($this->collapsed_tasks$i); ++ foreach (explode(',', $rec'id') as $rec_id) { ++ if (intval(get_input_value('collapsed', RCUBE_INPUT_GPC))) { ++ $this->collapsed_tasks = $rec_id; ++ } ++ else { ++ $i = array_search($rec_id, $this->collapsed_tasks); ++ if ($i !== false) ++ unset($this->collapsed_tasks$i); ++ } + } + + $this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks)))); +@@ -278,7 +283,7 @@ class tasklist extends rcube_plugin + foreach ($refresh as $i => $r) + $this->encode_task($refresh$i); + } +- $this->rc->output->command('plugin.refresh_task', $refresh); ++ $this->rc->output->command('plugin.update_task', $refresh); + } + } + +@@ -540,8 +545,17 @@ class tasklist extends rcube_plugin + + } + */ ++ $data = $this->tasks_data($this->driver->list_tasks($filter, $lists), $f, $tags); ++ $this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags)))); ++ } ++ ++ /** ++ * Prepare and sort the given task records to be sent to the client ++ */ ++ private function tasks_data($records, $f, &$tags) ++ { + $data = $tags = $this->task_tree = $this->task_titles = array(); +- foreach ($this->driver->list_tasks($filter, $lists) as $rec) { ++ foreach ($records as $rec) { + if ($rec'parent_id') { + $this->task_tree$rec'id' = $rec'parent_id'; + } +@@ -558,7 +572,7 @@ class tasklist extends rcube_plugin + array_walk($data, array($this, 'task_walk_tree')); + usort($data, array($this, 'task_sort_cmp')); + +- $this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags)))); ++ return $data; + } + + /** +@@ -605,6 +619,10 @@ class tasklist extends rcube_plugin + $rec'attachments'$k'classname' = rcube_utils::file2class($attachment'mimetype', $attachment'name'); + } + ++ if (!is_array($rec'tags')) ++ $rec'tags' = (array)$rec'tags'; ++ sort($rec'tags', SORT_LOCALE_STRING); ++ + if (in_array($rec'id', $this->collapsed_tasks)) + $rec'collapsed' = true; + +@@ -720,6 +738,34 @@ class tasklist extends rcube_plugin + exit; + } + ++ /** ++ * Handler for keep-alive requests ++ * This will check for updated data in active lists and sync them to the client ++ */ ++ public function refresh($attr) ++ { ++ // refresh the entire list every 10th time to also sync deleted items ++ if (rand(0,10) == 10) { ++ $this->rc->output->command('plugin.reload_data'); ++ return; ++ } ++ ++ $filter = array( ++ 'since' => $attr'last', ++ 'search' => get_input_value('q', RCUBE_INPUT_GPC), ++ 'mask' => intval(get_input_value('filter', RCUBE_INPUT_GPC)) & self::FILTER_MASK_COMPLETE, ++ ); ++ $lists = get_input_value('lists', RCUBE_INPUT_GPC);; ++ ++ $updates = $this->driver->list_tasks($filter, $lists); ++ if (!empty($updates)) { ++ $this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates, 255, $tags), true); ++ ++ // update counts ++ $counts = $this->driver->count_tasks($lists); ++ $this->rc->output->command('plugin.update_counts', $counts); ++ } ++ } + + /** + * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests. +diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js +index e3a889c..f804c34 100644 +--- a/plugins/tasklist/tasklist_base.js ++++ b/plugins/tasklist/tasklist_base.js +@@ -4,7 +4,7 @@ + * @version @package_version@ + * @author Thomas Bruederli <bruederli@kolabsys.com> + * +- * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com> ++ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as +@@ -37,10 +37,9 @@ function rcube_tasklist(settings) + /** + * Open a new task dialog prefilled with contents from the currently selected mail message + */ +- function create_from_mail() ++ function create_from_mail(uid) + { +- var uid; +- if ((uid = rcmail.get_single_uid())) { ++ if (uid || (uid = rcmail.get_single_uid())) { + // load calendar UI (scripts and edit dialog template) + if (!ui_loaded) { + $.when( +@@ -53,7 +52,7 @@ function rcube_tasklist(settings) + + ui_loaded = true; + me.ui = new rcube_tasklist_ui(settings); +- create_from_mail(); // start over ++ create_from_mail(uid); // start over + }); + return; + } +@@ -90,5 +89,14 @@ window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', fu + rcmail.env.message_commands.push('tasklist-create-from-mail'); + else + rcmail.enable_command('tasklist-create-from-mail', true); ++ ++ // add contextmenu item ++ if (window.rcm_contextmenu_register_command) { ++ rcm_contextmenu_register_command( ++ 'tasklist-create-from-mail', ++ function(cmd,el){ tasks.create_from_mail() }, ++ 'tasklist.createfrommail', ++ 'moveto'); ++ } + }); + +diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php +index dab9b12..21faba3 100644 +--- a/plugins/tasklist/tasklist_ui.php ++++ b/plugins/tasklist/tasklist_ui.php +@@ -117,7 +117,7 @@ class tasklist_ui + + $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class), + ($prop'virtual' ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list', 'value' => $id, 'checked' => $prop'active'))) . +- html::span('handle', ' ') . ++ html::span(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), ' ') . + html::span(array('class' => 'listname', 'title' => $title), $prop'listname')); + } +
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.