diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php index acc8fa09..9bd06254 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -26,6 +26,7 @@ class tasklist_database_driver extends tasklist_driver { public $undelete = true; // yes, we can public $sortable = false; + public $alarm_types = array('DISPLAY','EMAIL'); private $rc; private $plugin; @@ -367,14 +368,15 @@ class tasklist_database_driver extends tasklist_driver if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly']) return false; - foreach (array('parent_id', 'date', 'time') as $col) { + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) { if (empty($prop[$col])) $prop[$col] = null; } + $notify_at = $this->_get_notification($prop); $result = $this->rc->db->query(sprintf( "INSERT INTO " . $this->db_tasks . " - (tasklist_id, uid, parent_id, created, changed, title, date, time, description, tags) + (tasklist_id, uid, parent_id, created, changed, title, date, time, starttime, starttime, description, tags, alarms, notify) VALUES (?, ?, ?, %s, %s, ?, ?, ?, ?, ?)", $this->rc->db->now(), $this->rc->db->now() @@ -385,8 +387,12 @@ class tasklist_database_driver extends tasklist_driver $prop['title'], $prop['date'], $prop['time'], + $prop['startdate'], + $prop['starttime'], strval($prop['description']), - join(',', (array)$prop['tags']) + join(',', (array)$prop['tags']), + $prop['alarms'], + $notify_at ); if ($result) @@ -409,13 +415,18 @@ class tasklist_database_driver extends tasklist_driver if (isset($prop[$col])) $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]); } - foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime') as $col) { + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) { if (isset($prop[$col])) $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col])); } if (isset($prop['tags'])) $sql_set[] = $this->rc->db->quote_identifier('tags') . '=' . $this->rc->db->quote(join(',', (array)$prop['tags'])); + if (isset($prop['date']) || isset($prop['time']) || isset($prop['alarms'])) { + $notify_at = $this->_get_notification($prop); + $sql_set[] = $this->rc->db->quote_identifier('notify') . '=' . (empty($notify_at) ? 'NULL' : $this->rc->db->quote($notify_at)); + } + // moved from another list if ($prop['_fromlist'] && ($newlist = $prop['list'])) { $sql_set[] = 'tasklist_id=' . $this->rc->db->quote($newlist); @@ -495,4 +506,31 @@ class tasklist_database_driver extends tasklist_driver return $this->rc->db->affected_rows($query); } + /** + * Compute absolute time to notify the user + */ + private function _get_notification($task) + { + // fake object properties to suit the expectations of calendar::get_next_alarm() + // TODO: move all that to libcalendaring plugin + if ($task['date']) + $task['start'] = new DateTime($task['date'] . ' ' . ($task['time'] ?: '23:00'), $this->plugin->timezone); + if ($task['startdate']) + $task['end'] = new DateTime($task['startdate'] . ' ' . ($task['starttime'] ?: '06:00'), $this->plugin->timezone); + else + $task['end'] = $tast['start']; + + if (!$task['start']) + $task['end'] = $task['start']; + + if ($task['alarms'] && $task['start'] > new DateTime() || strpos($task['alarms'], '@') !== false) { + $alarm = calendar::get_next_alarm($task); + + if ($alarm['time'] && $alarm['action'] == 'DISPLAY') + return date('Y-m-d H:i:s', $alarm['time']); + } + + return null; + } + } diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index 13f51fa4..25d4c087 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -347,6 +347,10 @@ class tasklist_kolab_driver extends tasklist_driver $task['changed'] = $record['dtstamp']; } + if ($record['alarms']) { + $task['alarms'] = $record['alarms']; + } + if (!empty($record['_attachments'])) { foreach ($record['_attachments'] as $key => $attachment) { if ($attachment !== false) { diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc index ba7be2e9..35bae103 100644 --- a/plugins/tasklist/localization/en_US.inc +++ b/plugins/tasklist/localization/en_US.inc @@ -41,9 +41,9 @@ $labels['tabrecurrence'] = 'Recurrence'; $labels['tabattachments'] = 'Attachments'; $labels['tabsharing'] = 'Sharing'; -$labels['editlist'] = 'Edit resource'; -$labels['createlist'] = 'Add resource'; -$labels['listactions'] = 'Resource options...'; +$labels['editlist'] = 'Edit list'; +$labels['createlist'] = 'Add list'; +$labels['listactions'] = 'List options...'; $labels['listname'] = 'Name'; $labels['showalarms'] = 'Show alarms'; $labels['import'] = 'Import'; diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css index 32eee745..df82cc77 100644 --- a/plugins/tasklist/skins/larry/tasklist.css +++ b/plugins/tasklist/skins/larry/tasklist.css @@ -616,7 +616,7 @@ label.block { margin-bottom: 0.3em; } -#edit-completeness-slider { +#taskedit-completeness-slider { display: inline-block; margin-left: 2em; width: 30em; @@ -624,7 +624,7 @@ label.block { border: 1px solid #ccc; } -#edit-tagline { +#taskedit-tagline { width: 97%; } diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html index 773badf7..b347a0c0 100644 --- a/plugins/tasklist/skins/larry/templates/mainview.html +++ b/plugins/tasklist/skins/larry/templates/mainview.html @@ -112,6 +112,10 @@ +
+ + +
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html index 8cad89b5..1773fead 100644 --- a/plugins/tasklist/skins/larry/templates/taskedit.html +++ b/plugins/tasklist/skins/larry/templates/taskedit.html @@ -6,48 +6,52 @@
- +
- +
- +
- +
- - + +
- -   - - + +   + +
- -   - - + +   + + +
+
+ +
- -  % -
+ +  % +
- - + +
- +
- +
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index 2457b447..f393c464 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -60,6 +60,7 @@ function rcube_tasklist_ui(settings) var draghelper; var search_request; var search_query; + var completeness_slider; var me = this; // general datepicker settings @@ -93,22 +94,6 @@ function rcube_tasklist_ui(settings) this.unlock_saving = unlock_saving; - /* basic initializations */ - - $('#taskedit').tabs(); - - var completeness_slider = $('#edit-completeness-slider').slider({ - range: 'min', - slide: function(e, ui){ - var v = completeness_slider.slider('value'); - if (v >= 98) v = 100; - if (v <= 2) v = 0; - $('#edit-completeness').val(v); - } - }); - $('#edit-completeness').change(function(e){ completeness_slider.slider('value', parseInt(this.value)) }); - - /** * initialize the tasks UI */ @@ -312,6 +297,45 @@ function rcube_tasklist_ui(settings) }, datepicker_settings); } + /** + * initialize task edit form elements + */ + function init_taskedit() + { + $('#taskedit').tabs(); + + completeness_slider = $('#taskedit-completeness-slider').slider({ + range: 'min', + slide: function(e, ui){ + var v = completeness_slider.slider('value'); + if (v >= 98) v = 100; + if (v <= 2) v = 0; + $('#taskedit-completeness').val(v); + } + }); + $('#taskedit-completeness').change(function(e){ + completeness_slider.slider('value', parseInt(this.value)) + }); + + // register events on alarm fields + $('#taskedit select.edit-alarm-type').change(function(){ + $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')](); + }); + $('#taskedit select.edit-alarm-offset').change(function(){ + var mode = $(this).val() == '@' ? 'show' : 'hide'; + $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode](); + $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show'); + }); + + $('#taskedit-date, #taskedit-startdate, #taskedit .edit-alarm-date').datepicker(datepicker_settings); + + $('a.edit-nodate').click(function(){ + var sel = $(this).attr('rel'); + if (sel) $(sel).val(''); + return false; + }); + } + /** * Request counts from the server */ @@ -725,6 +749,7 @@ function rcube_tasklist_ui(settings) $('#task-time').html(Q(rec.time || '')); $('#task-start')[(rec.startdate ? 'show' : 'hide')]().children('.task-text').html(Q(rec.startdate || '')); $('#task-starttime').html(Q(rec.starttime || '')); + $('#task-alarm')[(rec.alarms_text ? 'show' : 'hide')]().children('.task-text').html(Q(rec.alarms_text)); $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%'); $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : '')); @@ -793,16 +818,19 @@ function rcube_tasklist_ui(settings) if (!me.selected_task.id) me.selected_task.id = -(++idcount); + // reset dialog first + $('#taskeditform').get(0).reset(); + // fill form data - var title = $('#edit-title').val(rec.title || ''); - var description = $('#edit-description').val(rec.description || ''); - var recdate = $('#edit-date').val(rec.date || '').datepicker(datepicker_settings); - var rectime = $('#edit-time').val(rec.time || ''); - var recstartdate = $('#edit-startdate').val(rec.startdate || '').datepicker(datepicker_settings); - var recstarttime = $('#edit-starttime').val(rec.starttime || ''); - var complete = $('#edit-completeness').val((rec.complete || 0) * 100); + var title = $('#taskedit-title').val(rec.title || ''); + var description = $('#taskedit-description').val(rec.description || ''); + var recdate = $('#taskedit-date').val(rec.date || ''); + var rectime = $('#taskedit-time').val(rec.time || ''); + var recstartdate = $('#taskedit-startdate').val(rec.startdate || ''); + var recstarttime = $('#taskedit-starttime').val(rec.starttime || ''); + var complete = $('#taskedit-completeness').val((rec.complete || 0) * 100); completeness_slider.slider('value', complete.val()); - var tasklist = $('#edit-tasklist').val(rec.list || 0).prop('disabled', rec.parent_id ? true : false); + var tasklist = $('#taskedit-tasklist').val(rec.list || 0).prop('disabled', rec.parent_id ? true : false); // tag-edit line var tagline = $(rcmail.gui_objects.edittagline).empty(); @@ -823,11 +851,32 @@ function rcube_tasklist_ui(settings) texts: { removeLinkTitle: rcmail.gettext('removetag', 'tasklist') } }); - $('a.edit-nodate').unbind('click').click(function(){ - var sel = $(this).attr('rel'); - if (sel) $(sel).val(''); - return false; - }) + // set alarm(s) + if (rec.alarms) { + if (typeof rec.alarms == 'string') + rec.alarms = rec.alarms.split(';'); + + for (var alarm, i=0; i < rec.alarms.length; i++) { + alarm = String(rec.alarms[i]).split(':'); + if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY'; + $('#taskedit select.edit-alarm-type').val(alarm[1]); + + if (alarm[0].match(/@(\d+)/)) { + var ondate = fromunixtime(parseInt(RegExp.$1)); + $('#taskedit select.edit-alarm-offset').val('@'); + $('#taskedit input.edit-alarm-date').val(format_datetime(ondate, 1)); + $('#taskedit input.edit-alarm-time').val(format_datetime(ondate, 2)); + } + else if (alarm[0].match(/([-+])(\d+)([MHD])/)) { + $('#taskedit input.edit-alarm-value').val(RegExp.$2); + $('#taskedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3); + } + + break; // only one alarm is currently supported + } + } + // set correct visibility by triggering onchange handlers + $('#taskedit select.edit-alarm-type, #taskedit select.edit-alarm-offset').change(); // attachments rcmail.enable_command('remove-attachment', !list.readonly); @@ -874,6 +923,16 @@ function rcube_tasklist_ui(settings) me.selected_task.tags.push(elem.value); }); + // serialize alarm settings + var alarm = $('#taskedit select.edit-alarm-type').val(); + if (alarm) { + var val, offset = $('#taskedit select.edit-alarm-offset').val(); + if (offset == '@') + me.selected_task.alarms = '@' + date2unixtime(parse_datetime($('#taskedit input.edit-alarm-time').val(), $('#taskedit input.edit-alarm-date').val())) + ':' + alarm; + else if ((val = parseInt($('#taskedit input.edit-alarm-value').val())) && !isNaN(val) && val >= 0) + me.selected_task.alarms = offset[0] + val + offset[1] + ':' + alarm; + } + // uploaded attachments list for (var i in rcmail.env.attachments) { if (i.match(/^rcmfile(.+)/)) @@ -1069,9 +1128,9 @@ function rcube_tasklist_ui(settings) list = { name:'', editable:true, showalarms:true }; // fill edit form - var name = $('#edit-tasklistame').prop('disabled', !list.editable).val(list.editname || list.name), - alarms = $('#edit-showalarms').prop('checked', list.showalarms).get(0), - parent = $('#edit-parentfolder').val(list.parentfolder); + var name = $('#taskedit-tasklistame').prop('disabled', !list.editable).val(list.editname || list.name), + alarms = $('#taskedit-showalarms').prop('checked', list.showalarms).get(0), + parent = $('#taskedit-parentfolder').val(list.parentfolder); // dialog buttons var buttons = {}; @@ -1351,6 +1410,73 @@ function rcube_tasklist_ui(settings) }) .data('id', id); } + + + /**** calendaring utility functions *****/ + /* TO BE MOVED TO libcalendaring plugin */ + + var gmt_offset = (new Date().getTimezoneOffset() / -60) - (rcmail.env.calendar_settings.timezone || 0) - (rcmail.env.calendar_settings.dst || 0); + var client_timezone = new Date().getTimezoneOffset(); + + /** + * from time and date strings to a real date object + */ + function parse_datetime(time, date) + { + // we use the utility function from datepicker to parse dates + var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date(); + + var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/); + if (!isNaN(time_arr[0])) { + date.setHours(time_arr[0]); + if (time.match(/p[.m]*/i) && date.getHours() < 12) + date.setHours(parseInt(time_arr[0]) + 12); + else if (time.match(/a[.m]*/i) && date.getHours() == 12) + date.setHours(0); + } + if (!isNaN(time_arr[1])) + date.setMinutes(time_arr[1]); + + return date; + } + + /** + * Format the given date object according to user's prefs + */ + function format_datetime(date, mode) + { + var format = + mode == 2 ? rcmail.env.calendar_settings['time_format'] : + (mode == 1 ? rcmail.env.calendar_settings['date_format'] : + rcmail.env.calendar_settings['date_format'] + ' '+ rcmail.env.calendar_settings['time_format']); + + return $.fullCalendar.formatDate(date, format); + } + + /** + * convert the given Date object into a unix timestamp respecting browser's and user's timezone settings + */ + function date2unixtime(date) + { + var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset + return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset); + } + + /** + * + */ + function fromunixtime(ts) + { + ts -= gmt_offset * 3600; + var date = new Date(ts * 1000), + dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; + if (dst_offset) // adjust DST offset + date.setTime((ts + 3600) * 1000); + return date; + } + + // init dialog by default + init_taskedit(); } diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index e1559d67..13635a70 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -301,6 +301,10 @@ class tasklist extends rcube_plugin } } + // alarms cannot work without a date + if ($rec['alarms'] && !$rec['date'] && !$rec['startdate'] && strpos($task['alarms'], '@') === false) + $rec['alarms'] = ''; + $attachments = array(); $taskid = $rec['id']; if (is_array($_SESSION['tasklist_session']) && $_SESSION['tasklist_session']['id'] == $taskid) { @@ -501,6 +505,9 @@ class tasklist extends rcube_plugin } } + if ($rec['alarms']) + $rec['alarms_text'] = calendar::alarms_text($rec['alarms']); + foreach ((array)$rec['attachments'] as $k => $attachment) { $rec['attachments'][$k]['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']); } diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php index 370237e5..330e7a63 100644 --- a/plugins/tasklist/tasklist_ui.php +++ b/plugins/tasklist/tasklist_ui.php @@ -81,6 +81,7 @@ 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.alarm_select', array($this, 'alarm_select')); $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')); @@ -156,22 +157,22 @@ class tasklist_ui { $fields = array( 'name' => array( - 'id' => 'edit-tasklistame', + 'id' => 'taskedit-tasklistame', 'label' => $this->plugin->gettext('listname'), - 'value' => html::tag('input', array('id' => 'edit-tasklistame', 'name' => 'name', 'type' => 'text', 'class' => 'text', 'size' => 40)), + 'value' => html::tag('input', array('id' => 'taskedit-tasklistame', 'name' => 'name', 'type' => 'text', 'class' => 'text', 'size' => 40)), ), /* 'color' => array( - 'id' => 'edit-color', + 'id' => 'taskedit-color', 'label' => $this->plugin->gettext('color'), - 'value' => html::tag('input', array('id' => 'edit-color', 'name' => 'color', 'type' => 'text', 'class' => 'text colorpicker', 'size' => 6)), - ), - 'showalarms' => array( - 'id' => 'edit-showalarms', - 'label' => $this->plugin->gettext('showalarms'), - 'value' => html::tag('input', array('id' => 'edit-showalarms', 'name' => 'color', 'type' => 'checkbox')), + 'value' => html::tag('input', array('id' => 'taskedit-color', 'name' => 'color', 'type' => 'text', 'class' => 'text colorpicker', 'size' => 6)), ), */ + 'showalarms' => array( + 'id' => 'taskedit-showalarms', + 'label' => $this->plugin->gettext('showalarms'), + 'value' => html::tag('input', array('id' => 'taskedit-showalarms', 'name' => 'color', 'type' => 'checkbox')), + ), ); return html::tag('form', array('action' => "#", 'method' => "post", 'id' => 'tasklisteditform'), @@ -180,18 +181,38 @@ class tasklist_ui } /** - * Render a HTML select box to select a task category + * Render HTML form for alarm configuration */ - function category_select($attrib = array()) + function alarm_select($attrib = array()) { - $attrib['name'] = 'categories'; - $select = new html_select($attrib); - $select->add('---', ''); - foreach ((array)$this->plugin->driver->list_categories() as $cat => $color) { - $select->add($cat, $cat); - } + unset($attrib['name']); + $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type')); + $select_type->add(rcube_label('none'), ''); + foreach ($this->plugin->driver->alarm_types as $type) + $select_type->add(rcube_label(strtolower("calendar.alarm{$type}option")), $type); - return $select->show(null); + $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3)); + $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10)); + $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6)); + + $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset')); + foreach (array('-M','-H','-D','+M','+H','+D','@') as $trigger) + $select_offset->add(rcube_label('calendar.trigger' . $trigger), $trigger); + + // pre-set with default values from user settings + $preset = calendar::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M')); + $hidden = array('style' => 'display:none'); + $html = html::span('edit-alarm-set', + $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' . + html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'), + $input_value->show($preset[0]) . ' ' . + $select_offset->show($preset[1]) . ' ' . + $input_date->show('', $hidden) . ' ' . + $input_time->show('', $hidden) + ) + ); + + return $html; } /** @@ -226,7 +247,7 @@ class tasklist_ui */ function tagslist($attrib) { - $attrib += array('id' => 'rcmtagslist'); + $attrib += array('id' => 'rcmtasktagslist'); unset($attrib['name']); $this->rc->output->add_gui_object('tagslist', $attrib['id']); @@ -238,7 +259,7 @@ class tasklist_ui */ function tags_editline($attrib) { - $attrib += array('id' => 'rcmtagsedit'); + $attrib += array('id' => 'rcmtasktagsedit'); $this->rc->output->add_gui_object('edittagline', $attrib['id']); $input = new html_inputfield(array('name' => 'tags[]', 'class' => 'tag', 'size' => $attrib['size'], 'tabindex' => $attrib['tabindex'])); @@ -251,7 +272,7 @@ class tasklist_ui function attachments_list($attrib = array()) { if (!$attrib['id']) - $attrib['id'] = 'rcmAttachmentList'; + $attrib['id'] = 'rcmtaskattachmentlist'; $this->rc->output->add_gui_object('attachmentlist', $attrib['id']); @@ -265,7 +286,7 @@ class tasklist_ui { // add ID if not given if (!$attrib['id']) - $attrib['id'] = 'rcmUploadForm'; + $attrib['id'] = 'rcmtaskuploadform'; // Get max filesize, enable upload progress bar $max_filesize = rcube_upload_init();