From 05a92b0b928a3e31f9c4cc56b0c51ac428d16f43 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Wed, 1 Aug 2012 15:52:28 +0200 Subject: [PATCH] Add support for task attachments (#895) --- .../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 +++++- .../skins/larry/templates/attachment.html | 36 +++ .../skins/larry/templates/mainview.html | 4 + .../skins/larry/templates/taskedit.html | 84 +++--- plugins/tasklist/tasklist.js | 141 +++++++++- plugins/tasklist/tasklist.php | 247 ++++++++++++++++++ plugins/tasklist/tasklist_ui.php | 84 ++++++ 11 files changed, 775 insertions(+), 44 deletions(-) create mode 100644 plugins/tasklist/skins/larry/templates/attachment.html diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index 7322e1f3..c3a220a4 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; } @@ -480,6 +534,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; + } + /** * */ diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php index c86eca8d..924da21e 100644 --- a/plugins/tasklist/drivers/tasklist_driver.php +++ b/plugins/tasklist/drivers/tasklist_driver.php @@ -174,6 +174,34 @@ abstract class tasklist_driver return false; } + /** + * 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 a702a70d..9d21692f 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 ccf93584..ba7be2e9 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 ad7d05a8..32eee745 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 00000000..4d4789da --- /dev/null +++ b/plugins/tasklist/skins/larry/templates/attachment.html @@ -0,0 +1,36 @@ + + + +<roundcube:object name="pagetitle" /> + + + + + + +
+
+ +
+ +
+ +
+ +
+ + + + diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html index 114b7ec5..773badf7 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -120,6 +120,10 @@ +
+ +
+
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html index cdce8963..8cad89b5 100644 --- a/plugins/tasklist/skins/larry/templates/taskedit.html +++ b/plugins/tasklist/skins/larry/templates/taskedit.html @@ -1,39 +1,55 @@ -
+
-
- -
- +
    +
  • +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ + +
+
+ +   + + +
+
+ +   + + +
+
+ +  % +
+
+
+ + +
-
- -
- -
-
- - -
-
- -   - - -
-
- -   - - -
-
- -  % -
-
-
- - + +
+
+ +
+
+ +
+
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 39636d6f..e5a874aa 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 = $('