2 commits - plugins/calendar plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Wed Aug 1 15:52:36 CEST 2012


 plugins/calendar/calendar.php                            |    1 
 plugins/calendar/drivers/kolab/kolab_calendar.php        |    1 
 plugins/calendar/lib/calendar_ui.php                     |    2 
 plugins/calendar/skins/classic/calendar.css              |    2 
 plugins/calendar/skins/larry/calendar.css                |    2 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |  106 ++++++
 plugins/tasklist/drivers/tasklist_driver.php             |   28 +
 plugins/tasklist/localization/de_CH.inc                  |    5 
 plugins/tasklist/localization/en_US.inc                  |    5 
 plugins/tasklist/skins/larry/tasklist.css                |   79 ++++
 plugins/tasklist/skins/larry/templates/attachment.html   |   36 ++
 plugins/tasklist/skins/larry/templates/mainview.html     |    4 
 plugins/tasklist/skins/larry/templates/taskedit.html     |   84 +++--
 plugins/tasklist/tasklist.js                             |  141 ++++++++
 plugins/tasklist/tasklist.php                            |  247 +++++++++++++++
 plugins/tasklist/tasklist_ui.php                         |   84 +++++
 16 files changed, 778 insertions(+), 49 deletions(-)

New commits:
commit 05a92b0b928a3e31f9c4cc56b0c51ac428d16f43
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Aug 1 15:52:28 2012 +0200

    Add support for task attachments (#895)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 7322e1f..c3a220a 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -26,7 +26,7 @@ class tasklist_kolab_driver extends tasklist_driver
 {
     // features supported by the backend
     public $alarms = false;
-    public $attachments = false;
+    public $attachments = true;
     public $undelete = false; // task undelete action
 
     private $rc;
@@ -346,6 +346,18 @@ class tasklist_kolab_driver extends tasklist_driver
             $task['changed'] = $record['dtstamp'];
         }
 
+        if (!empty($record['_attachments'])) {
+            foreach ($record['_attachments'] as $key => $attachment) {
+                if ($attachment !== false) {
+                    if (!$attachment['name'])
+                        $attachment['name'] = $key;
+                    $attachments[] = $attachment;
+                }
+            }
+
+            $task['attachments'] = $attachments;
+        }
+
         return $task;
     }
 
@@ -387,6 +399,47 @@ class tasklist_kolab_driver extends tasklist_driver
             $object[$key] = $val;
         }
 
+        // delete existing attachment(s)
+        if (!empty($task['deleted_attachments'])) {
+            foreach ($task['deleted_attachments'] as $attachment) {
+                if (is_array($object['_attachments'])) {
+                    foreach ($object['_attachments'] as $idx => $att) {
+                        if ($att['id'] == $attachment)
+                            $object['_attachments'][$idx] = false;
+                    }
+                }
+            }
+            unset($task['deleted_attachments']);
+        }
+
+        // in kolab_storage attachments are indexed by content-id
+        if (is_array($task['attachments'])) {
+            foreach ($task['attachments'] as $idx => $attachment) {
+                $key = null;
+                // Roundcube ID has nothing to do with the storage ID, remove it
+                if ($attachment['content']) {
+                    unset($attachment['id']);
+                }
+                else {
+                    foreach ((array)$old['_attachments'] as $cid => $oldatt) {
+                        if ($oldatt && $attachment['id'] == $oldatt['id'])
+                            $key = $cid;
+                    }
+                }
+
+                // replace existing entry
+                if ($key) {
+                    $object['_attachments'][$key] = $attachment;
+                }
+                // append as new attachment
+                else {
+                    $object['_attachments'][] = $attachment;
+                }
+            }
+
+            unset($task['attachments']);
+        }
+
         unset($object['tempid'], $object['raw']);
         return $object;
     }
@@ -442,7 +495,8 @@ class tasklist_kolab_driver extends tasklist_driver
             $saved = false;
         }
         else {
-            $task['id'] = $task['uid'];
+            $task = $this->_to_rcube_task($object);
+            $task['list'] = $list_id;
             $this->tasks[$task['uid']] = $task;
         }
 
@@ -481,6 +535,54 @@ class tasklist_kolab_driver extends tasklist_driver
 
 
     /**
+     * Get attachment properties
+     *
+     * @param string $id    Attachment identifier
+     * @param array  $task  Hash array with event properties:
+     *         id: Task identifier
+     *       list: List identifier
+     *
+     * @return array Hash array with attachment properties:
+     *         id: Attachment identifier
+     *       name: Attachment name
+     *   mimetype: MIME content type of the attachment
+     *       size: Attachment size
+     */
+    public function get_attachment($id, $task)
+    {
+        $task['uid'] = $task['id'];
+        $task = $this->get_task($task);
+
+        if ($task && !empty($task['attachments'])) {
+            foreach ($task['attachments'] as $att) {
+                if ($att['id'] == $id)
+                    return $att;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Get attachment body
+     *
+     * @param string $id    Attachment identifier
+     * @param array  $task  Hash array with event properties:
+     *         id: Task identifier
+     *       list: List identifier
+     *
+     * @return string Attachment body
+     */
+    public function get_attachment_body($id, $task)
+    {
+        if ($storage = $this->folders[$task['list']]) {
+            return $storage->get_attachment($task['id'], $id);
+        }
+
+        return false;
+    }
+
+    /**
      * 
      */
     public function tasklist_edit_form($formfields)
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index c86eca8..924da21 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -175,6 +175,34 @@ abstract class tasklist_driver
     }
 
     /**
+     * Get attachment properties
+     *
+     * @param string $id    Attachment identifier
+     * @param array  $task  Hash array with event properties:
+     *         id: Task identifier
+     *       list: List identifier
+     *
+     * @return array Hash array with attachment properties:
+     *         id: Attachment identifier
+     *       name: Attachment name
+     *   mimetype: MIME content type of the attachment
+     *       size: Attachment size
+     */
+    public function get_attachment($id, $task) { }
+
+    /**
+     * Get attachment body
+     *
+     * @param string $id    Attachment identifier
+     * @param array  $task  Hash array with event properties:
+     *         id: Task identifier
+     *       list: List identifier
+     *
+     * @return string Attachment body
+     */
+    public function get_attachment_body($id, $task) { }
+
+    /**
      * List availabale categories
      * The default implementation reads them from config/user prefs
      */
diff --git a/plugins/tasklist/localization/de_CH.inc b/plugins/tasklist/localization/de_CH.inc
index a702a70..9d21692 100644
--- a/plugins/tasklist/localization/de_CH.inc
+++ b/plugins/tasklist/localization/de_CH.inc
@@ -36,6 +36,11 @@ $labels['save'] = 'Speichern';
 $labels['cancel'] = 'Abbrechen';
 $labels['addsubtask'] = 'Neue Teilaufgabe';
 
+$labels['tabsummary'] = 'Ãœbersicht';
+$labels['tabrecurrence'] = 'Wiederholung';
+$labels['tabattachments'] = 'Anhänge';
+$labels['tabsharing'] = 'Freigabe';
+
 $labels['editlist'] = 'Ressource bearbeiten';
 $labels['createlist'] = 'Neue Ressource';
 $labels['listactions'] = 'Ressourcenoptionen...';
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index ccf9358..ba7be2e 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -36,6 +36,11 @@ $labels['save'] = 'Save';
 $labels['cancel'] = 'Cancel';
 $labels['addsubtask'] = 'Add subtask';
 
+$labels['tabsummary'] = 'Summary';
+$labels['tabrecurrence'] = 'Recurrence';
+$labels['tabattachments'] = 'Attachments';
+$labels['tabsharing'] = 'Sharing';
+
 $labels['editlist'] = 'Edit resource';
 $labels['createlist'] = 'Add resource';
 $labels['listactions'] = 'Resource options...';
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index ad7d05a..32eee74 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -373,13 +373,14 @@ ul.toolbarmenu li span.icon.taskadd {
 }
 
 .taskhead .tags {
+	display: block;
 	position: absolute;
-	top: 4px;
+	top: 3px;
 	right: 110px;
 	max-width: 14em;
+	height: 16px;
 	overflow: hidden;
 	padding-top: 1px;
-	padding-bottom: 4px;
 	text-align: right;
 }
 
@@ -388,7 +389,7 @@ ul.toolbarmenu li span.icon.taskadd {
 	background: #d9ecf4;
 	border: 1px solid #c2dae5;
 	border-radius: 4px;
-	padding: 2px 8px;
+	padding: 1px 7px;
 	margin-right: 3px;
 }
 
@@ -529,6 +530,13 @@ ul.toolbarmenu li span.delete {
 	display:none;
 }
 
+#taskedit {
+	position: relative;
+	top: -1.5em;
+	padding: 0.5em 0.1em;
+	margin: 0 -0.2em;
+}
+
 #taskshow h2 {
 	margin-top: -0.5em;
 }
@@ -553,11 +561,43 @@ a.morelink:hover {
 	text-decoration: underline;
 }
 
+#taskedit .ui-tabs-panel {
+	min-height: 24em;
+}
+
 #taskeditform input.text,
 #taskeditform textarea {
 	width: 97%;
 }
 
+#taskeditform .formbuttons {
+	margin: 0.5em 0;
+}
+
+#taskedit-attachments {
+	margin: 0.6em 0;
+}
+
+#taskedit-attachments ul li {
+	display: block;
+	color: #333;
+	font-weight: bold;
+	padding: 8px 4px 3px 30px;
+	text-shadow: 0px 1px 1px #fff;
+	text-decoration: none;
+	white-space: nowrap;
+}
+
+#taskedit-attachments ul li a.file {
+	padding: 0;
+}
+
+#taskedit-attachments-form {
+	margin-top: 1em;
+	padding-top: 0.8em;
+	border-top: 2px solid #fafafa;
+}
+
 div.form-section {
 	position: relative;
 	margin-top: 0.2em;
@@ -568,6 +608,7 @@ div.form-section {
 	display: inline-block;
 	min-width: 7em;
 	padding-right: 0.5em;
+	margin-bottom: 0.3em;
 }
 
 label.block {
@@ -587,6 +628,38 @@ label.block {
 	width: 97%;
 }
 
+#taskedit .droptarget {
+	background-image: url(../../../../skins/larry/images/filedrop.png) !important;
+	background-position: center bottom !important;
+	background-repeat: no-repeat !important;
+}
+
+#taskedit .droptarget.hover,
+#taskedit .droptarget.active {
+	border-color: #019bc6;
+	box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+	-moz-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+	-webkit-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+	-o-box-shadow: 0 0 3px 2px rgba(71,135,177, 0.5);
+}
+
+#taskedit .droptarget.hover {
+	background-color: #d9ecf4;
+	box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+	-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+	-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+	-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+}
+
+#task-attachments .attachmentslist li {
+	float: left;
+	margin-right: 1em;
+}
+
+#task-attachments .attachmentslist li a {
+	outline: none;
+}
+
 
 /**
  * Styles of the tagedit inputsforms
diff --git a/plugins/tasklist/skins/larry/templates/attachment.html b/plugins/tasklist/skins/larry/templates/attachment.html
new file mode 100644
index 0000000..4d4789d
--- /dev/null
+++ b/plugins/tasklist/skins/larry/templates/attachment.html
@@ -0,0 +1,36 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+</head>
+<body class="extwin">
+
+<div id="header">
+	<div id="topline">
+		<div class="topright">
+			<a href="#close" class="closelink" onclick="self.close()"><roundcube:label name="close" /></a>
+		</div>
+	</div>
+
+	<div id="topnav">
+		<roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" />
+	</div>
+
+	<br style="clear:both" />
+</div>
+
+<div id="mainscreen">
+	<div id="partheader" class="uibox">
+	<roundcube:object name="plugin.attachmentcontrols" class="headers-table" />
+	</div>
+
+	<div id="attachmentcontainer" class="uibox">
+	<roundcube:object name="plugin.attachmentframe" id="attachmentframe" class="header-table" style="width:100%" />
+	</div>
+
+</div>
+
+</body>
+</html>
+
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 114b7ec..773badf 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -120,6 +120,10 @@
 		<label><roundcube:label name="tasklist.complete" /></label>
 		<span class="task-text"></span>
 	</div>
+	<div id="task-attachments" class="form-section">
+		<label><roundcube:label name="attachments" /></label>
+		<div class="task-text"></div>
+	</div>
 </div>
 
 <roundcube:include file="/templates/taskedit.html" />
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index cdce896..8cad89b 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -1,39 +1,55 @@
-<div id="taskedit">
+<div id="taskedit" class="uidialog uidialog-tabbed">
 	<form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
-		<div class="form-section">
-			<label for="edit-title"><roundcube:label name="tasklist.title" /></label>
-			<br />
-			<input type="text" class="text" name="title" id="edit-title" size="60" tabindex="1" />
+		<ul>
+			<li><a href="#taskedit-tab-1"><roundcube:label name="tasklist.tabsummary" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-tab-2"><roundcube:label name="tasklist.tabattachments" /></a></li>
+		</ul>
+		<!-- basic info -->
+		<div id="taskedit-tab-1">
+			<div class="form-section">
+				<label for="edit-title"><roundcube:label name="tasklist.title" /></label>
+				<br />
+				<input type="text" class="text" name="title" id="edit-title" size="60" tabindex="1" />
+			</div>
+			<div class="form-section">
+				<label for="edit-description"><roundcube:label name="tasklist.description" /></label>
+				<br />
+				<textarea name="description" id="edit-description" class="text" rows="5" cols="60" tabindex="2"></textarea>
+			</div>
+			<div class="form-section">
+				<label for="edit-tags"><roundcube:label name="tasklist.tags" /></label>
+				<roundcube:object name="plugin.tags_editline" id="edit-tagline" class="tagedit" tabindex="3" />
+			</div>
+			<div class="form-section">
+				<label for="edit-date"><roundcube:label name="tasklist.datetime" /></label>
+				<input type="text" name="date" size="10" id="edit-date" tabindex="20" />  
+				<input type="text" name="time" size="6" id="edit-time" tabindex="21" />
+				<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#edit-date,#edit-time"><roundcube:label name="tasklist.nodate" /></a>
+			</div>
+			<div class="form-section">
+				<label for="edit-startdate"><roundcube:label name="tasklist.start" /></label>
+				<input type="text" name="startdate" size="10" id="edit-startdate" tabindex="23" />  
+				<input type="text" name="starttime" size="6" id="edit-starttime" tabindex="24" />
+				<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#edit-startdate,#edit-starttime"><roundcube:label name="tasklist.nodate" /></a>
+			</div>
+			<div class="form-section">
+				<label for="edit-completeness"><roundcube:label name="tasklist.complete" /></label>
+				<input type="text" name="title" id="edit-completeness" size="3"  tabindex="25" /> %
+				<div id="edit-completeness-slider"></div>
+			</div>
+			<div class="form-section" id="tasklist-select">
+				<label for="edit-tasklist"><roundcube:label name="tasklist.list" /></label>
+				<roundcube:object name="plugin.tasklist_select" id="edit-tasklist" tabindex="26" />
+			</div>
 		</div>
-		<div class="form-section">
-			<label for="edit-description"><roundcube:label name="tasklist.description" /></label>
-			<br />
-			<textarea name="description" id="edit-description" class="text" rows="5" cols="60" tabindex="2"></textarea>
-		</div>
-		<div class="form-section">
-			<label for="edit-tags"><roundcube:label name="tasklist.tags" /></label>
-			<roundcube:object name="plugin.tags_editline" id="edit-tagline" class="tagedit" tabindex="3" />
-		</div>
-		<div class="form-section">
-			<label for="edit-date"><roundcube:label name="tasklist.datetime" /></label>
-			<input type="text" name="date" size="10" id="edit-date" tabindex="20" />  
-			<input type="text" name="time" size="6" id="edit-time" tabindex="21" />
-			<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#edit-date,#edit-time"><roundcube:label name="tasklist.nodate" /></a>
-		</div>
-		<div class="form-section">
-			<label for="edit-startdate"><roundcube:label name="tasklist.start" /></label>
-			<input type="text" name="startdate" size="10" id="edit-startdate" tabindex="23" />  
-			<input type="text" name="starttime" size="6" id="edit-starttime" tabindex="24" />
-			<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#edit-startdate,#edit-starttime"><roundcube:label name="tasklist.nodate" /></a>
-		</div>
-		<div class="form-section">
-			<label for="edit-completeness"><roundcube:label name="tasklist.complete" /></label>
-			<input type="text" name="title" id="edit-completeness" size="3"  tabindex="25" /> %
-			<div id="edit-completeness-slider"></div>
-		</div>
-		<div class="form-section" id="tasklist-select">
-			<label for="edit-tasklist"><roundcube:label name="tasklist.list" /></label>
-			<roundcube:object name="plugin.tasklist_select" id="edit-tasklist" tabindex="26" />
+		<!-- attachments list (with upload form) -->
+		<div id="taskedit-tab-2">
+			<div id="taskedit-attachments">
+				<roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" />
+			</div>
+			<div id="taskedit-attachments-form">
+				<roundcube:object name="plugin.attachments_form" id="tasklist-attachment-form" attachmentFieldSize="30" />
+			</div>
+			<roundcube:object name="plugin.filedroparea" id="taskedit-tab-2" />
 		</div>
 	</form>
 </div>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 39636d6..e5a874a 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -95,6 +95,8 @@ function rcube_tasklist_ui(settings)
 
     /* basic initializations */
 
+    $('#taskedit').tabs();
+
     var completeness_slider = $('#edit-completeness-slider').slider({
         range: 'min',
         slide: function(e, ui){
@@ -568,6 +570,7 @@ function rcube_tasklist_ui(settings)
         }
 
         // remove from list index
+        var oldlist = listindex.join('%%%');
         var oldindex = listindex.indexOf(rec.id);
         if (oldindex >= 0) {
             slice = listindex.slice(0,oldindex);
@@ -610,6 +613,9 @@ function rcube_tasklist_ui(settings)
             slice.push(rec.id);
             listindex = slice.concat(listindex.slice(index));
         }
+        else {  // restore old list index
+            listindex = oldlist.split('%%%');
+        }
     }
 
     /**
@@ -729,6 +735,15 @@ function rcube_tasklist_ui(settings)
             });
         }
 
+        // build attachments list
+        $('#task-attachments').hide();
+        if ($.isArray(rec.attachments)) {
+            task_show_attachments(rec.attachments || [], $('#task-attachments').children('.task-text'), rec);
+            if (rec.attachments.length > 0) {
+                $('#task-attachments').show();
+          }
+        }
+
         // define dialog buttons
         var buttons = {};
         buttons[rcmail.gettext('edit','tasklist')] = function() {
@@ -748,7 +763,7 @@ function rcube_tasklist_ui(settings)
           closeOnEscape: true,
           title: rcmail.gettext('taskdetails', 'tasklist'),
           close: function() {
-            $dialog.dialog('destroy').appendTo(document.body);
+              $dialog.dialog('destroy').appendTo(document.body);
           },
           buttons: buttons,
           minWidth: 500,
@@ -769,11 +784,15 @@ function rcube_tasklist_ui(settings)
             list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] :
                 (me.selected_list ? me.tasklists[me.selected_list] : { editable: action=='new' });
 
-        if (list.readonly || (action == 'edit' && (!rec || rec.readonly || rec.temp)))
+        if (list.readonly || (action == 'edit' && (!rec || rec.readonly)))
             return false;
 
         me.selected_task = $.extend({}, rec);  // clone task object
 
+        // assign temporary id
+        if (!me.selected_task.id)
+            me.selected_task.id = -(++idcount);
+
         // fill form data
         var title = $('#edit-title').val(rec.title || '');
         var description = $('#edit-description').val(rec.description || '');
@@ -810,6 +829,26 @@ function rcube_tasklist_ui(settings)
             return false;
         })
 
+        // attachments
+        rcmail.enable_command('remove-attachment', !list.readonly);
+        me.selected_task.deleted_attachments = [];
+        // we're sharing some code for uploads handling with app.js
+        rcmail.env.attachments = [];
+        rcmail.env.compose_id = me.selected_task.id; // for rcmail.async_upload_form()
+
+        if ($.isArray(rec.attachments)) {
+            task_show_attachments(rec.attachments, $('#taskedit-attachments'), rec, true);
+        }
+        else {
+            $('#taskedit-attachments > ul').empty();
+        }
+
+        // show/hide tabs according to calendar's feature support
+        $('#taskedit-tab-attachments')[(list.attachments?'show':'hide')]();
+
+        // activate the first tab
+        $('#eventtabs').tabs('select', 0);
+
         // define dialog buttons
         var buttons = {};
         buttons[rcmail.gettext('save', 'tasklist')] = function() {
@@ -818,6 +857,7 @@ function rcube_tasklist_ui(settings)
                 me.selected_task[key] = input.val();
             });
             me.selected_task.tags = [];
+            me.selected_task.attachments = [];
 
             // do some basic input validation
             if (me.selected_task.startdate && me.selected_task.date) {
@@ -834,8 +874,14 @@ function rcube_tasklist_ui(settings)
                     me.selected_task.tags.push(elem.value);
             });
 
+            // uploaded attachments list
+            for (var i in rcmail.env.attachments) {
+                if (i.match(/^rcmfile(.+)/))
+                    me.selected_task.attachments.push(RegExp.$1);
+            }
+
             if (me.selected_task.list && me.selected_task.list != rec.list)
-              me.selected_task._fromlist = rec.list;
+                me.selected_task._fromlist = rec.list;
 
             me.selected_task.complete = complete.val() / 100;
             if (isNaN(me.selected_task.complete))
@@ -866,8 +912,8 @@ function rcube_tasklist_ui(settings)
           closeOnEscape: false,
           title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'),
           close: function() {
-            editform.hide().appendTo(document.body);
-            $dialog.dialog('destroy').remove();
+              editform.hide().appendTo(document.body);
+              $dialog.dialog('destroy').remove();
           },
           buttons: buttons,
           minHeight: 340,
@@ -878,6 +924,91 @@ function rcube_tasklist_ui(settings)
         title.select();
     }
 
+
+    /**
+     * Open a task attachment either in a browser window for inline view or download it
+     */
+    function load_attachment(rec, att)
+    {
+        var qstring = '_id='+urlencode(att.id)+'&_t='+urlencode(rec.recurrence_id||rec.id)+'&_list='+urlencode(rec.list);
+
+        // open attachment in frame if it's of a supported mimetype
+        // similar as in app.js and calendar_ui.js
+        if (att.id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
+            rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubetaskattachment');
+            if (rcmail.attachment_win) {
+                window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10);
+                return;
+            }
+        }
+
+        rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
+    };
+
+    /**
+     * Build task attachments list
+     */
+    function task_show_attachments(list, container, rec, edit)
+    {
+        var i, id, len, content, li, elem,
+            ul = $('<ul>').addClass('attachmentslist');
+
+        for (i=0, len=list.length; i<len; i++) {
+            elem = list[i];
+            li = $('<li>').addClass(elem.classname);
+
+            if (edit) {
+                rcmail.env.attachments[elem.id] = elem;
+                // delete icon
+                content = $('<a>')
+                    .attr('href', '#delete')
+                    .attr('title', rcmail.gettext('delete'))
+                    .addClass('delete')
+                    .click({ id:elem.id }, function(e) {
+                        remove_attachment(this, e.data.id);
+                        return false;
+                    });
+
+                if (!rcmail.env.deleteicon) {
+                    content.html(rcmail.gettext('delete'));
+                }
+                else {
+                    $('<img>').attr('src', rcmail.env.deleteicon).attr('alt', rcmail.gettext('delete')).appendTo(content);
+                }
+
+                li.append(content);
+            }
+
+            // name/link
+            $('<a>')
+                .attr('href', '#load')
+                .addClass('file')
+                .html(elem.name).click({ task:rec, att:elem }, function(e) {
+                    load_attachment(e.data.task, e.data.att);
+                    return false;
+                }).appendTo(li);
+
+            ul.append(li);
+        }
+
+        if (edit && rcmail.gui_objects.attachmentlist) {
+            ul.id = rcmail.gui_objects.attachmentlist.id;
+            rcmail.gui_objects.attachmentlist = ul.get(0);
+        }
+
+        container.empty().append(ul);
+    };
+
+    /**
+     *
+     */
+    var remove_attachment = function(elem, id)
+    {
+        $(elem.parentNode).hide();
+        me.selected_task.deleted_attachments.push(id);
+        delete rcmail.env.attachments[id];
+    };
+
     /**
      *
      */
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 4247e14..6dec0d4 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -83,6 +83,8 @@ class tasklist extends rcube_plugin
             $this->register_action('fetch', array($this, 'fetch_tasks'));
             $this->register_action('inlineui', array($this, 'get_inline_ui'));
             $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'));
         }
         else if ($this->rc->task == 'mail') {
             // TODO: register hooks to catch ical/vtodo email attachments
@@ -296,6 +298,24 @@ class tasklist extends rcube_plugin
             }
         }
 
+        $attachments = array();
+        $taskid = $rec['id'];
+        if (is_array($_SESSION['tasklist_session']) && $_SESSION['tasklist_session']['id'] == $taskid) {
+            if (!empty($_SESSION['tasklist_session']['attachments'])) {
+                foreach ($_SESSION['tasklist_session']['attachments'] as $id => $attachment) {
+                    if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
+                        $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
+                        unset($attachments[$id]['abort'], $attachments[$id]['group']);
+                    }
+                }
+            }
+        }
+
+        $rec['attachments'] = $attachments;
+
+        if (is_numeric($rec['id']) && $rec['id'] < 0)
+            unset($rec['id']);
+
         return $rec;
     }
 
@@ -464,6 +484,10 @@ class tasklist extends rcube_plugin
             }
         }
 
+        foreach ((array)$rec['attachments'] as $k => $attachment) {
+            $rec['attachments'][$k]['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']);
+        }
+
         if (!isset($rec['_depth'])) {
             $rec['_depth'] = 0;
             $parent_id = $this->task_tree[$rec['id']];
@@ -571,6 +595,229 @@ class tasklist extends rcube_plugin
     }
 
 
+    /******* Attachment handling  *******/
+    /*** pretty much the same as in plugins/calendar/calendar.php ***/
+
+    /**
+     * Handler for attachments upload
+    */
+    public function attachment_upload()
+    {
+        // Upload progress update
+        if (!empty($_GET['_progress'])) {
+            rcube_upload_progress();
+        }
+
+        $taskid = get_input_value('_id', RCUBE_INPUT_GPC);
+        $uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
+
+        // prepare session storage
+        if (!is_array($_SESSION['tasklist_session']) || $_SESSION['tasklist_session']['id'] != $taskid) {
+            $_SESSION['tasklist_session'] = array();
+            $_SESSION['tasklist_session']['id'] = $taskid;
+            $_SESSION['tasklist_session']['attachments'] = array();
+        }
+
+        // clear all stored output properties (like scripts and env vars)
+        $this->rc->output->reset();
+
+        if (is_array($_FILES['_attachments']['tmp_name'])) {
+            foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
+                // Process uploaded attachment if there is no error
+                $err = $_FILES['_attachments']['error'][$i];
+
+                if (!$err) {
+                    $attachment = array(
+                        'path' => $filepath,
+                        'size' => $_FILES['_attachments']['size'][$i],
+                        'name' => $_FILES['_attachments']['name'][$i],
+                        'mimetype' => rc_mime_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
+                        'group' => $eventid,
+                    );
+
+                    $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
+                }
+
+                if (!$err && $attachment['status'] && !$attachment['abort']) {
+                    $id = $attachment['id'];
+
+                    // store new attachment in session
+                    unset($attachment['status'], $attachment['abort']);
+                    $_SESSION['tasklist_session']['attachments'][$id] = $attachment;
+
+                    $content = html::a(array(
+                        'href' => "#delete",
+                        'class' => 'delete',
+                        'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
+                        'title' => rcube_label('delete'),
+                    ), Q(rcube_label('delete')));
+
+                    $content .= Q($attachment['name']);
+
+                    $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
+                        'html' => $content,
+                        'name' => $attachment['name'],
+                        'mimetype' => $attachment['mimetype'],
+                        'classname' => rcmail_filetype2classname($attachment['mimetype'], $attachment['name']),
+                        'complete' => true), $uploadid);
+                }
+                else {  // upload failed
+                    if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+                        $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
+                            'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+                    }
+                    else if ($attachment['error']) {
+                        $msg = $attachment['error'];
+                    }
+                    else {
+                        $msg = rcube_label('fileuploaderror');
+                    }
+
+                    $this->rc->output->command('display_message', $msg, 'error');
+                    $this->rc->output->command('remove_from_attachment_list', $uploadid);
+                }
+            }
+        }
+        else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+            // if filesize exceeds post_max_size then $_FILES array is empty,
+            // show filesizeerror instead of fileuploaderror
+            if ($maxsize = ini_get('post_max_size')) {
+                $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
+                    'size' => show_bytes(parse_bytes($maxsize)))));
+            }
+            else {
+                $msg = rcube_label('fileuploaderror');
+            }
+
+            $this->rc->output->command('display_message', $msg, 'error');
+            $this->rc->output->command('remove_from_attachment_list', $uploadid);
+        }
+
+        $this->rc->output->send('iframe');
+    }
+
+    /**
+     * Handler for attachments download/displaying
+     */
+    public function attachment_get()
+    {
+        $task = get_input_value('_t', RCUBE_INPUT_GPC);
+        $list = get_input_value('_list', RCUBE_INPUT_GPC);
+        $id   = get_input_value('_id', RCUBE_INPUT_GPC);
+
+        $task = array('id' => $task, 'list' => $list);
+
+        // show loading page
+        if (!empty($_GET['_preload'])) {
+          $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
+          $message = rcube_label('loadingdata');
+
+          header('Content-Type: text/html; charset=' . RCMAIL_CHARSET);
+          print "<html>\n<head>\n"
+              . '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
+              . '<meta http-equiv="content-type" content="text/html; charset='.RCMAIL_CHARSET.'">' . "\n"
+              . "</head>\n<body>\n$message\n</body>\n</html>";
+          exit;
+        }
+
+        ob_end_clean();
+
+        $attachment = $this->attachment = $this->driver->get_attachment($id, $task);
+
+        // show part page
+        if (!empty($_GET['_frame'])) {
+            $this->register_handler('plugin.attachmentframe', array($this, 'attachment_frame'));
+            $this->register_handler('plugin.attachmentcontrols', array($this->ui, 'attachment_controls'));
+            $this->rc->output->send('tasklist.attachment');
+            exit;
+        }
+
+        if ($attachment) {
+            // allow post-processing of the attachment body
+            $part = new rcube_message_part;
+            $part->filename  = $attachment['name'];
+            $part->size      = $attachment['size'];
+            $part->mimetype  = $attachment['mimetype'];
+
+            $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
+                'body' => $this->driver->get_attachment_body($id, $task),
+                'mimetype' => strtolower($attachment['mimetype']),
+                'download' => !empty($_GET['_download']),
+                'part' => $part,
+            ));
+
+            if ($plugin['abort'])
+                exit;
+
+            $mimetype = $plugin['mimetype'];
+            list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+            $browser = $this->rc->output->browser;
+
+            // send download headers
+            if ($plugin['download']) {
+                header("Content-Type: application/octet-stream");
+                if ($browser->ie)
+                    header("Content-Type: application/force-download");
+            }
+            else if ($ctype_primary == 'text') {
+                header("Content-Type: text/$ctype_secondary");
+            }
+            else {
+                header("Content-Type: $mimetype");
+                header("Content-Transfer-Encoding: binary");
+            }
+
+            // display page, @TODO: support text/plain (and maybe some other text formats)
+            if ($mimetype == 'text/html' && empty($_GET['_download'])) {
+                $OUTPUT = new rcube_html_page();
+                // @TODO: use washtml on $body
+                $OUTPUT->write($plugin['body']);
+            }
+            else {
+                // don't kill the connection if download takes more than 30 sec.
+                @set_time_limit(0);
+
+                $filename = $attachment['name'];
+                $filename = preg_replace('[\r\n]', '', $filename);
+
+                if ($browser->ie && $browser->ver < 7)
+                    $filename = rawurlencode(abbreviate_string($filename, 55));
+                else if ($browser->ie)
+                    $filename = rawurlencode($filename);
+                else
+                    $filename = addcslashes($filename, '"');
+
+                $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
+                header("Content-Disposition: $disposition; filename=\"$filename\"");
+
+                echo $plugin['body'];
+            }
+
+            exit;
+        }
+
+        // if we arrive here, the requested part was not found
+        header('HTTP/1.1 404 Not Found');
+        exit;
+    }
+
+    /**
+     * Template object for attachment display frame
+     */
+    public function attachment_frame($attrib)
+    {
+        $attachment = $this->attachment;
+
+        $mimetype = strtolower($attachment['mimetype']);
+        list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+        $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
+
+        return html::iframe($attrib);
+    }
+
+
     /*******  Email related function *******/
 
     public function mail_message2task()
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 43f79c9..370237e 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -81,6 +81,16 @@ class tasklist_ui
         $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview'));
         $this->plugin->register_handler('plugin.tagslist', array($this, 'tagslist'));
         $this->plugin->register_handler('plugin.tags_editline', array($this, 'tags_editline'));
+        $this->plugin->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
+        $this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
+        $this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
+
+        // define list of file types which can be displayed inline
+        // same as in program/steps/mail/show.inc
+        $mimetypes = $this->rc->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
+        $settings = $this->rc->output->get_env('tasklist_settings');
+        $settings['mimetypes'] = is_string($mimetypes) ? explode(',', $mimetypes) : (array)$mimetypes;
+        $this->rc->output->set_env('tasklist_settings', $settings);
 
         $this->plugin->include_script('jquery.tagedit.js');
         $this->plugin->include_script('tasklist.js');
@@ -102,6 +112,7 @@ class tasklist_ui
             $prop['alarms'] = $this->plugin->driver->alarms;
             $prop['undelete'] = $this->plugin->driver->undelete;
             $prop['sortable'] = $this->plugin->driver->sortable;
+            $prop['attachments'] = $this->plugin->driver->attachments;
             $jsenv[$id] = $prop;
 
             $html_id = html_identifier($id);
@@ -234,4 +245,77 @@ class tasklist_ui
         return html::div($attrib, $input->show(''));
     }
 
+    /**
+     * Generate HTML element for attachments list
+     */
+    function attachments_list($attrib = array())
+    {
+        if (!$attrib['id'])
+            $attrib['id'] = 'rcmAttachmentList';
+
+        $this->rc->output->add_gui_object('attachmentlist', $attrib['id']);
+
+        return html::tag('ul', $attrib, '', html::$common_attrib);
+    }
+
+    /**
+     * Generate the form for event attachments upload
+     */
+    function attachments_form($attrib = array())
+    {
+        // add ID if not given
+        if (!$attrib['id'])
+            $attrib['id'] = 'rcmUploadForm';
+
+        // Get max filesize, enable upload progress bar
+        $max_filesize = rcube_upload_init();
+
+        $button = new html_inputfield(array('type' => 'button'));
+        $input = new html_inputfield(array(
+            'type' => 'file',
+            'name' => '_attachments[]',
+            'multiple' => 'multiple',
+            'size' => $attrib['attachmentfieldsize'],
+        ));
+
+        return html::div($attrib,
+            html::div(null, $input->show()) .
+            html::div('formbuttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
+                'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) .
+            html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
+        );
+    }
+
+    /**
+     * Register UI object for HTML5 drag & drop file upload
+     */
+    function file_drop_area($attrib = array())
+    {
+        if ($attrib['id']) {
+            $this->rc->output->add_gui_object('filedrop', $attrib['id']);
+            $this->rc->output->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments'));
+        }
+    }
+
+    /**
+     *
+     */
+    function attachment_controls($attrib = array())
+    {
+        $table = new html_table(array('cols' => 3));
+
+        if (!empty($this->plugin->attachment['name'])) {
+            $table->add('title', Q(rcube_label('filename')));
+            $table->add('header', Q($this->plugin->attachment['name']));
+            $table->add('download-link', html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))));
+        }
+
+      if (!empty($this->plugin->attachment['size'])) {
+        $table->add('title', Q(rcube_label('filesize')));
+        $table->add('header', Q(show_bytes($this->plugin->attachment['size'])));
+      }
+
+      return $table->show($attrib);
+    }
+
 }


commit a16a9c581079516e100c00d24ede77efc3bcc038
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Aug 1 15:47:50 2012 +0200

    Small code and style cleanup

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 77719c7..335c672 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1469,7 +1469,6 @@ class calendar extends rcube_plugin
     }
 
     $event    = get_input_value('_id', RCUBE_INPUT_GPC);
-    $calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
     $uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
 
     $eventid = 'cal:'.$event;
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 91ba5b8..813b391 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -462,7 +462,6 @@ class kolab_calendar
     // in kolab_storage attachments are indexed by content-id
     $object['_attachments'] = array();
     if (is_array($event['attachments'])) {
-      $collisions = array();
       foreach ($event['attachments'] as $idx => $attachment) {
         $key = null;
         // Roundcube ID has nothing to do with the storage ID, remove it
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index a063a4d..1cbcf0a 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -623,7 +623,7 @@ class calendar_ui
 
     return html::div($attrib,
       html::div(null, $input->show()) .
-      html::div('buttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
+      html::div('formbuttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
         'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) .
       html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
     );
diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 21dcb1b..e4afea4 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -364,7 +364,7 @@ a.miniColors-trigger {
 	padding-top: 1.2em;
 }
 
-#edit-attachments-form .buttons {
+#edit-attachments-form .formbuttons {
 	margin: 0.5em 0;
 }
 
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index d0d108d..6765211 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -371,7 +371,7 @@ a.miniColors-trigger {
 	border-top: 2px solid #fafafa;
 }
 
-#edit-attachments-form .buttons {
+#edit-attachments-form .formbuttons {
 	margin: 0.5em 0;
 }
 





More information about the commits mailing list