From adc41ebf61bdac6c59beb9796a6881beaad9392c Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Mon, 13 Oct 2014 18:40:39 +0200 Subject: [PATCH] - List linked tasks in mail view, like notes - Deep-links into the tasks view opening the linked task - Mark tasks as complete directly from mail view --- .../drivers/kolab/tasklist_kolab_driver.php | 19 +++++ plugins/tasklist/drivers/tasklist_driver.php | 18 ++++- plugins/tasklist/skins/larry/buttons.png | Bin 2232 -> 3264 bytes plugins/tasklist/skins/larry/tasklist.css | 40 +++++++++++ plugins/tasklist/tasklist.js | 21 +++++- plugins/tasklist/tasklist.php | 67 +++++++++++++++++- plugins/tasklist/tasklist_base.js | 28 ++++++++ plugins/tasklist/tasklist_ui.php | 17 +++++ 8 files changed, 204 insertions(+), 6 deletions(-) diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index e44b2c6a..3ee1100e 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -1237,6 +1237,25 @@ class tasklist_kolab_driver extends tasklist_driver return $this->_convert_message_uri($uri); } + /** + * Find tasks assigned to a specified message + * + * @see tasklist_driver::get_message_related_tasks() + */ + public function get_message_related_tasks($headers, $folder) + { + $config = kolab_storage_config::get_instance(); + $result = $config->get_message_relations($headers, $folder, 'task'); + + foreach ($result as $idx => $rec) { + $task = $this->_to_rcube_task($rec); + $task['list'] = kolab_storage::folder_id($rec['_mailbox']); + $result[$idx] = $task; + } + + return $result; + } + /** * */ diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php index 2102d219..791d2ab4 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -290,8 +290,8 @@ abstract class tasklist_driver /** * Build a URI representing the given message reference * - * @param object rcube_message_header Instance holding the message headers - * @param string IMAP folder the message resides in + * @param object $headers rcube_message_header instance holding the message headers + * @param string $folder IMAP folder the message resides in * * @return string An URI referencing the given IMAP message */ @@ -301,6 +301,20 @@ abstract class tasklist_driver return false; } + /** + * Find tasks assigned to a specified message + * + * @param object $message rcube_message_header instance + * @param string $folder IMAP folder the message resides in + * + * @param array List of linked task objects + */ + public function get_message_related_tasks($headers, $folder) + { + // to be implemented by the derived classes + return array(); + } + /** * Helper method to determine whether the given task is considered "complete" * diff --git a/plugins/tasklist/skins/larry/buttons.png b/plugins/tasklist/skins/larry/buttons.png index 62baed675464a49fd751f6ecb563d463732c93ff..7f1a2b6331a3f2aae7aff7a3288dc3a052b71fe9 100644 GIT binary patch literal 3264 zcmV;x3_tUUP)k`Re_N4(nm8E-!wYcvRz}D5$odP<1IFuO%VP2_uq~> zFTm3Y{s?eh-v>Upbm>yUqeqX#{rmUD?c29SpFVw3QQyO^35lYMi;J@q6ch+z^5n@& zP*+u%f2eR8uj&p8>60fILb96EGp7lA@zk`kQ%GiS~mdinCDc>46I$j{Fg4<0-a{{H^QA;TZ|4K!zf)~JD< zw1rZMw`|#Bp)jg%;>3xIQP*M7qD84BR!BMj6v{RnEs3##+(6sm)X?zZ!y94)$>qy$ z-MTf2a!V!Nym_;Q!e}g$CQXV&U2|X>1UyiPNy@3<3$pFbojV_+%^aW+=N&s%Fkgx? zC{K!b)22-pE(?HG%{l(WK^_717 z)yEMRSvAlSSc=`~l|sx>>Gk>poDLEKEN<+_{*WV291{~`fuXRInwW*;tueZow6wGW z63?DJ`!ebbz{fyEH8J~B4&=!ZZ``=ig7wJFfX1aJX8AQo2l7qp*RQ|Exd-Y4@2T3j zu^;5RCdV+(8p1OcAvolH&4{*(iGa>Qn{*@ABtZuIQ5RkdZR z$tyeR-*+OTD*LfNUVa!Vtobq>dReR%ciRqnllSM1f&Q1_!8%A?%Uh-oN)0>Xg3 zKytak{**&`lq*qaI37SNKwoaKKjl!KtGJ3{T-^`l9_;>)LeWhEY}X^@iM2)Wf+A1i z%jv6Y^*)jxhpW6N(Qd;(quQhtiWY}$qS>L6(Hw0)TYoC2a-IKr5}w0m>->`PE_XOy zD0s!QoF~yDDq+uONjBl0XcP7J+eFPJhYM>(CEJRl_EJkh&57Usz?1O)?!x}BEH=>; zFV^zAtMZ;ilX+W{e}B*>J_Ye_`-GOsljykXO2Ris3I+Q&_&)FTuNpR&_aqvJ#ar01 z5%7tRyeC1@)<+A)A4qz@CaU=klJ_Lk#7&Z*>Y%qhiJ-&xoq7^#QfjL9r24@x_$owF8rK6i=ee#5EP+v~)eLZKMC=NqEO) zMk}N=M$T)cYBh^I3I9FW8-3zco`fv1d*7*%^#X=;(W^$p`oE-rJpRlf);D&n0Dkt- zH(iT7iB=2a4L(>>e_o!b@+9Qv*zQTx9T}NgAA)uJi^w_pB2uE(=%q!Tgh%%g4ci@f zklWINbv}Q)+LMqUvEoV8nHBed1SD5RsF2o;*!z|z(QN9fV3JY><&~M3bFB6xYR`<# zSICKaK>gTk8|L*bPr_r+tT1jMkW+r*;+};2x{OyOBr(-rrCFOFcoOV~{u&UgJc&wG zYsBcH(+jBT8U|x=PeOZQwI>1cO}go3C!U0M#L}LGro^7};>zVouwS`}8v*>L>A3H9 zM%Tl6CuPC*=!gDgBd*&hBzm9nwa5+P^JJU&(jpJGXFv9*9LSURB-|J6f2jNrpn&>i z0Nb-4`$LY)lW-MRah0+coX{s53C#2VKAk#s(gg$r@Y&d}Uq9olS+j0OMn(!=`OJEL?&s&H zcS;IE{jg!f#Q5>!d8Ak(MvopXMvWTf&3;3M4DlX3crc&seT_z=ATh6eW<5^(1=>V9 zB@PY_j>M^~z^S}|s!9arknm@}>C>nCBa~n#3JVJp3l=QksXecJw%4N#;~XZZ#1LfS zo8pvRtX{o33xSVNMBAJqF)=YTAtAw?{e%!z0Y3$c7cb7l{0d(AtU|+g5-(i1&_1Fy zYu1=4uoV28 zW$a4`OKu}aPSO!0M$l62FquqIAo{?uu8jzQ>7gZ0DPk-M6V0CBy<>^}jn9~Ss$Y-{ zo5Di`wn)gfT(i5>qr(OxdiU-v@X{$##4rexG6LQ^)Ycf5o=Jn7J9jRL?4u6}3E^JM z=Pb5S%fU<0u&5z9k4q!_VZc z8VyY#6a?>;dahTTy!*o1vq(ynQ}Fif+ow8P%QQCbWMjvUl_N$&)3|Zt1i^dO6(`4t zvq-vX)v9&eM4ZW?s=1$Iy z0m1>NI~SLX%9xxLep^vv^2BIp;&LGd3>a{*LOF`Jw|BsxL4)jC!}^ya2B8U!`KJ86 zB(MR9cJ10p!^_KyYgbU!G%$=@ju-|p4PVwIW@nC?n}sZx!XY?XYK#U3S#reOeXu@v z;RQ0Wmdg!C{}aeL7$7}IG#ub74e&WorGl%timSMatGLSlPZ2`sOMvH(LW*3yWx{!3 z*5aeHXU`gxw#ebqYetORoeYqRQ>RW@ksfPNqkH%6D;Qm4J0upFnK*{bMQzBDzhh;R zAZ`PloJm(_8xYz_FJ8RJ2)j-bVt5W#CJ3rj|CV4R1e%eb$85D1ty{OAftS=~&f$<* znH=aKH8@fv4SYtHq8ZtUOd6yUA9L*3v9IxbTnk=$iB={CqPT(dF${_cq0CfGRuV^< zugkf{$j`97Cd9}jwo*7Fr8{=)NFp&+&-7Qm(7AKxvN_*)89m#fy*9*1*0wUEl=mb^ zNl7_Hb#v5E>kAJLk5T*pndpI3JKAeQj7(E2jYm}#aP;U=lGEFWrY2aqd-v`@=3=6~ zHpEsG=2xognlx$B2mW!qdg;JH4`e$u*KWkv9GIuM6M~JnHzFA@JT*0S74HewBQfwS z?zK}j{vD90WDb`ES~%01G1uQ~wQKsoYuq3MoJoIc-_bNIIw^1qbrn}}6<7J6DEKw0 zM>xPK@R{e2LW+~RWjIXbNo;ZOtTG?x+Ci$Aq+ZK#?7tYQ!~fS#;9nlG_``)A_-8;Y zevOP0LfH3b94DH26p2WUHsl~pLjt~VoYFN^Lpim>*`=8W=LqR#@!(uDVg@Fa=fJ=~ zo%(YevXYcR(5LvTN-sQXGv|oBq%ji8djz`-JWA%3&j@gt8F%1R?Ao{Le9A%1Mc9G@mJpbasMD4qgo$=sP3=-?GZ_Upib0~s`7NRVM)ZX$oz>fog@{G!l3 yd-jCl`c7cJLy^cZPDwt{ioB`}T!D|0%D(|Zgn+=Dd*^)s0000#8y@)$5nR_WuGNTkPlOcX)Vs^xWUy|0t1m3bM7eJ<2hyD|9*? zVD8?%`@hG>#>SpMe;yGL0UlB)6tMo6=LklnQbm&}rWF$b0RcrtMR|F7rKP1lK0cnF zo<{(H$K&x+Q&VeeYs<^amX?;>+}uYz;8k&PaZF6i*RNkO7))qrsJFK_IOyWy0s!OV zW!)!B(bn1&~W+e;ybZI5;?PbaWI8 zFB0|3GH+f1oMLxyKpAvBy`!T8F#H>Vc5rYQ8XA%d%1TX5#bUAMH8VDp)w;SmP?*Z2 zA~KmA92`s{k=O*GA|fJQUS5NPgQ()c-rim*dw(zpcXxNAoOhr|fR~Yx0ge3n^$XK-I*=L|}=is3yI4rKKfc3=Il%Z*LE11?KDO z>MGyN1n#x7v(sirWk)NE$&vRm!N{kIfBh?ko9b+=j^FSw+ z(o|nx^QNk@yrkqsPEJmCR#sYKA|Vd<;JyXY01jhrZT(`bEH5w2|D2tfnH(AE>uzpt zY^biTs;DT-&(F`xOG(1TMuq_k1^)HG-P!q?iJmmvSN16c@-MQP@ulmg-78-LtOTz? zx;NOry}B7HOP1v462eEvjB<{GDIGzw5Wi5Xws#oOr`Y1 z3)>cG

hnV^!LrJQ&ETeK+LLXBDcc2w@FzuH8%u!ug@18ua5^h3MB_y4UTS zPG=vpGjYv6sphk=K19@5zE?uR730pwPosG)9Jy}#Cg`Ftm(R|i-5v3d&7)Ms{pMvU)X3kv z8gB8$gE!#K!baj6w^Dr~AF?K$X=4&z@zO-o+@DiMI)2{^mQ&t3LwqhI^U`QipdU-o z+r_i5EJRoGN!M}=vzGtB`np7FcaV@bOA-C?@P=*7eAf?jTrusuKo3a}-P-{+_%13* zMZF=t;^xG^%ANlw*7a7PMsPnUj*kVmsy`(;nKQJT#$wke@ZzF?*>`U-SIQ zT6?2Q2*2cg!$WO!!Nvz9@1&PrJKK(^5cEyihZ%H1zCgGNB8au!#8nROm2nsGLxhJp zJ1zdAME@Ph+8Tar{cW9-x8{A1lg_@bdO_v1<~o{Zv-OhbPx5`WOR|$%6Y>QFt%@YQp+OtW(^=&zaFLD@v5NU>DCk49VzIHt# zut*}rJ<=xT`>4=Qr`EWnPjyb5TRClOAD*dIpm*sy2h>SXxtrfsMzuVxj-ha+lRk&> zp!(_N-nQdugrij}k4G=gZafy5S8{a`jn)_b^K$iq>~K!wa!iVVM?c3x qZ?j_ouCzZ_(p#c@bUKPsU4deRo7oKB*93oHh}mTe<8nji*na>nFiUa( diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css index bf53e6b8..832197db 100644 --- a/plugins/tasklist/skins/larry/tasklist.css +++ b/plugins/tasklist/skins/larry/tasklist.css @@ -1337,6 +1337,46 @@ div.tasklist-invitebox .rsvp-status.accepted, background-position: 2px -220px; } +div.messagetasklinks { + margin: 8px 8px; + padding: 4px 8px; + border: 1px solid #dfdfdf; + background: #fafafa; + border-radius: 4px; +} + +div.messagetasklinks span.messagetaskref { + position: relative; + display: inline-block; + margin-right: 1em; + padding-right: 24px; +} + +div.messagetasklinks a.messagetasklink { + position: relative; + display: inline-block; + color: #333; + font-weight: bold; + padding: 3px 0 2px 22px; + text-shadow: 0px 1px 1px #fff; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 16em; + background: url(buttons.png) -6px -115px no-repeat; +} + +div.messagetasklinks span.messagetaskref.complete a.messagetasklink { + text-decoration: line-through; +} + +div.messagetasklinks span.messagetaskref input.complete { + position: absolute; + top: 1px; + right: 2px; +} + /** Special hacks for IE7 **/ /** They need to be in this file to also affect the task-create dialog embedded in mail view **/ diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 3cadd57a..5ec467d0 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -137,6 +137,11 @@ function rcube_tasklist_ui(settings) { // initialize task list selectors for (var id in me.tasklists) { + if (settings.selected_list && me.tasklists[settings.selected_list] && !me.tasklists[settings.selected_list].active) { + me.tasklists[settings.selected_list].active = true; + me.selected_list = settings.selected_list; + $(rcmail.gui_objects.tasklistslist).find("input[value='"+settings.selected_list+"']").prop('checked', true); + } if (me.tasklists[id].editable && (!me.selected_list || me.tasklists[id].default || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) { me.selected_list = id; } @@ -269,10 +274,9 @@ function rcube_tasklist_ui(settings) $('#taskviewsortmenu .by-' + (settings.sort_col || 'auto')).attr('aria-checked', 'true').addClass('selected'); $('#taskviewsortmenu .sortorder.' + (settings.sort_order || 'asc')).attr('aria-checked', 'true').addClass('selected'); - // start loading tasks fetch_counts(); - list_tasks(); + list_tasks(settings.selected_filter); // register event handlers for UI elements $('#taskselector a').click(function(e) { @@ -769,6 +773,19 @@ function rcube_tasklist_ui(settings) append_tags(response.tags || []); render_tasklist(); + // show selected task dialog + if (settings.selected_id) { + if (listdata[settings.selected_id]) { + task_show_dialog(settings.selected_id); + delete settings.selected_id; + } + + // remove _id from window location + if (window.history.replaceState) { + window.history.replaceState({}, document.title, rcmail.url('', { _list: me.selected_list })); + } + } + rcmail.set_busy(false, 'loading', ui_loading); } diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index 9acded6d..82b36343 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -61,6 +61,7 @@ class tasklist extends rcube_plugin public $home; // declare public to be used in other classes private $collapsed_tasks = array(); + private $message_tasks = array(); private $itip; private $ical; @@ -125,6 +126,9 @@ class tasklist extends rcube_plugin } else if ($args['task'] == 'mail') { if ($args['action'] == 'show' || $args['action'] == 'preview') { + if ($this->rc->config->get('tasklist_mail_embed', true)) { + $this->add_hook('message_load', array($this, 'mail_message_load')); + } $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html')); } @@ -204,7 +208,8 @@ class tasklist extends rcube_plugin $success = $refresh = false; // force notify if hidden + active - if ((int)$this->rc->config->get('calendar_itip_send_option', 3) === 1 && empty($rec['_reportpartstat'])) + $itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3); + if ($itip_send_option === 1 && empty($rec['_reportpartstat'])) $rec['_notify'] = 1; switch ($action) { @@ -220,6 +225,24 @@ class tasklist extends rcube_plugin } break; + case 'complete': + $complete = intval(rcube_utils::get_input_value('complete', rcube_utils::INPUT_POST)); + if (!($rec = $this->driver->get_task($rec))) { + break; + } + + $rec['status'] = $complete ? 'COMPLETED' : ($rec['complete'] > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION'); + + // sent itip notifications if enabled (no user interaction here) + if (($itip_send_option & 1)) { + if ($this->is_attendee($rec)) { + $rec['_reportpartstat'] = $rec['status']; + } + else if ($this->is_organizer($rec)) { + $rec['_notify'] = 1; + } + } + case 'edit': $rec = $this->prepare_task($rec); $clone = $this->handle_recurrence($rec, $this->driver->get_task($rec)); @@ -1437,7 +1460,36 @@ class tasklist extends rcube_plugin } } - // prepend event boxes to message body + // list linked tasks + $links = array(); + foreach ($this->message_tasks as $task) { + $checkbox = new html_checkbox(array( + 'name' => 'completed', + 'class' => 'complete', + 'title' => $this->gettext('complete'), + 'data-list' => $task['list'], + )); + $complete = $this->driver->is_complete($task); + $links[] = html::span('messagetaskref' . ($complete ? ' complete' : ''), + $checkbox->show($complete ? $task['uid'] : null, array('value' => $task['uid'])) . ' ' . + html::a(array( + 'href' => $this->rc->url(array( + 'task' => 'tasks', + 'list' => $task['list'], + 'id' => $task['uid'], + 'complete' => $complete?1:null, + )), + 'class' => 'messagetasklink', + 'rel' => $task['uid'] . '@' . $task['list'], + 'target' => '_blank', + ), Q($task['title'])) + ); + } + if (count($links)) { + $html .= html::div('messagetasklinks', join("\n", $links)); + } + + // prepend iTip/relation boxes to message body if ($html) { $this->load_ui(); $this->ui->init(); @@ -1465,6 +1517,17 @@ class tasklist extends rcube_plugin return $p; } + /** + * Lookup backend storage and find notes associated with the given message + */ + public function mail_message_load($p) + { + if (!$p['object']->headers->others['x-kolab-type']) { + $this->load_driver(); + $this->message_tasks = $this->driver->get_message_related_tasks($p['object']->headers, $p['object']->folder); + } + } + /** * Load iCalendar functions */ diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js index e399c537..b5ea06e9 100644 --- a/plugins/tasklist/tasklist_base.js +++ b/plugins/tasklist/tasklist_base.js @@ -30,6 +30,7 @@ function rcube_tasklist(settings) /* private vars */ var ui_loaded = false; var me = this; + var mywin = window; /* public members */ this.ui; @@ -79,6 +80,18 @@ function rcube_tasklist(settings) function mail2task_dialog(prop) { this.ui.edit_task(null, 'new', prop); + rcmail.addEventListener('responseaftertask', refresh_mailview); + } + + /** + * Reload the mail view/preview to update the tasks listing + */ + function refresh_mailview(e) + { + var win = rcmail.env.contentframe ? rcmail.get_frame_window(rcmail.env.contentframe) : mywin; + if (win && e.response.action == 'task') { + win.location.reload(); + } } // handler for attachment-save-tasklist commands @@ -95,6 +108,21 @@ function rcube_tasklist(settings) } } + // register event handlers on linked task items in message view + // the checkbox allows to mark a task as complete + if (rcmail.env.action == 'show' || rcmail.env.action == 'preview') { + $('div.messagetasklinks input.complete').click(function(e) { + var $this = $(this); + $(this).closest('.messagetaskref').toggleClass('complete'); + + // submit change to server + rcmail.http_post('tasks/task', { + action: 'complete', + t: { id:this.value, list:$this.attr('data-list') }, + complete: this.checked?1:0 + }, rcmail.set_busy(true, 'tasklist.savingdata')); + }); + } } /* tasklist plugin initialization (for email task) */ diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index b7bb4d43..5451e2cd 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -92,6 +92,23 @@ class tasklist_ui 'emails' => ';' . strtolower(join(';', $identity['emails'])) ); + if ($list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC)) { + $settings['selected_list'] = $list; + } + if ($list && ($id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC))) { + $settings['selected_id'] = $id; + + // check if the referenced task is completed + $task = $this->plugin->driver->get_task(array('id' => $id, 'list' => $list)); + console($id, $task); + if ($task && $this->plugin->driver->is_complete($task)) { + $settings['selected_filter'] = 'complete'; + } + } + else if ($filter = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GPC)) { + $settings['selected_filter'] = $filter; + } + return $settings; }