Handle recurring tasks (#2713)
- Render recurrence form as new tab in edit dialog - Display recurrence summary in task details - When marking a recurring task complete: * shift dates and alarms to next occurrence * only if recurrence end reached save as completed * save a copy with status completed (sort of a journal)
This commit is contained in:
parent
cd40e54641
commit
a0ac82793b
8 changed files with 161 additions and 14 deletions
|
@ -587,19 +587,22 @@ class tasklist_kolab_driver extends tasklist_driver
|
|||
'flagged' => $record['priority'] == 1,
|
||||
'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100),
|
||||
'parent_id' => $record['parent_id'],
|
||||
'recurrence' => $record['recurrence'],
|
||||
);
|
||||
|
||||
// convert from DateTime to internal date format
|
||||
if (is_a($record['due'], 'DateTime')) {
|
||||
$task['date'] = $record['due']->format('Y-m-d');
|
||||
$due = $this->plugin->lib->adjust_timezone($record['due']);
|
||||
$task['date'] = $due->format('Y-m-d');
|
||||
if (!$record['due']->_dateonly)
|
||||
$task['time'] = $record['due']->format('H:i');
|
||||
$task['time'] = $due->format('H:i');
|
||||
}
|
||||
// convert from DateTime to internal date format
|
||||
if (is_a($record['start'], 'DateTime')) {
|
||||
$task['startdate'] = $record['start']->format('Y-m-d');
|
||||
$start = $this->plugin->lib->adjust_timezone($record['start']);
|
||||
$task['startdate'] = $start->format('Y-m-d');
|
||||
if (!$record['start']->_dateonly)
|
||||
$task['starttime'] = $record['start']->format('H:i');
|
||||
$task['starttime'] = $start->format('H:i');
|
||||
}
|
||||
if (is_a($record['dtstamp'], 'DateTime')) {
|
||||
$task['changed'] = $record['dtstamp'];
|
||||
|
@ -661,13 +664,12 @@ class tasklist_kolab_driver extends tasklist_driver
|
|||
|
||||
// copy meta data (starting with _) from old object
|
||||
foreach ((array)$old as $key => $val) {
|
||||
if (!isset($object[$key]) && $key[0] == '_')
|
||||
$object[$key] = $val;
|
||||
if (!isset($object[$key]) && $key[0] == '_')
|
||||
$object[$key] = $val;
|
||||
}
|
||||
|
||||
// copy recurrence rules as long as the web client doesn't support it.
|
||||
// that way it doesn't get removed when saving through the web client (#2713)
|
||||
if ($old['recurrence']) {
|
||||
// copy recurrence rules if the client didn't submit it (#2713)
|
||||
if (!array_key_exists('recurrence', $object) && $old['recurrence']) {
|
||||
$object['recurrence'] = $old['recurrence'];
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ $labels['description'] = 'Description';
|
|||
$labels['datetime'] = 'Due';
|
||||
$labels['start'] = 'Start';
|
||||
$labels['alarms'] = 'Reminder';
|
||||
$labels['repeat'] = 'Repeat';
|
||||
|
||||
$labels['all'] = 'All';
|
||||
$labels['flagged'] = 'Flagged';
|
||||
|
|
|
@ -748,6 +748,12 @@ a.morelink:hover {
|
|||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
#taskedit .border-after {
|
||||
padding-bottom: 0.8em;
|
||||
margin-bottom: 0.8em;
|
||||
border-bottom: 2px solid #fafafa;
|
||||
}
|
||||
|
||||
#taskedit-attachments {
|
||||
margin: 0.6em 0;
|
||||
}
|
||||
|
|
|
@ -125,6 +125,10 @@
|
|||
<span class="task-text"></span>
|
||||
<span id="task-time"></span>
|
||||
</div>
|
||||
<div id="task-recurrence" class="form-section">
|
||||
<label><roundcube:label name="tasklist.repeat" /></label>
|
||||
<span class="task-text"></span>
|
||||
</div>
|
||||
<div id="task-alarm" class="form-section">
|
||||
<label><roundcube:label name="tasklist.alarms" /></label>
|
||||
<span class="task-text"></span>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<div id="taskedit" class="uidialog uidialog-tabbed">
|
||||
<form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
|
||||
<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>
|
||||
<li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
|
||||
</ul>
|
||||
<!-- basic info -->
|
||||
<div id="taskedit-tab-1">
|
||||
<div id="taskedit-panel-main">
|
||||
<div class="form-section">
|
||||
<label for="taskedit-title"><roundcube:label name="tasklist.title" /></label>
|
||||
<br />
|
||||
|
@ -51,8 +51,32 @@
|
|||
<roundcube:object name="plugin.tasklist_select" id="taskedit-tasklist" tabindex="26" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- recurrence settings -->
|
||||
<div id="taskedit-panel-recurrence">
|
||||
<div class="form-section border-after">
|
||||
<roundcube:object name="plugin.recurrence_form" part="frequency" />
|
||||
</div>
|
||||
<div class="recurrence-form border-after" id="recurrence-form-daily">
|
||||
<roundcube:object name="plugin.recurrence_form" part="daily" class="form-section" />
|
||||
</div>
|
||||
<div class="recurrence-form border-after" id="recurrence-form-weekly">
|
||||
<roundcube:object name="plugin.recurrence_form" part="weekly" class="form-section" />
|
||||
</div>
|
||||
<div class="recurrence-form border-after" id="recurrence-form-monthly">
|
||||
<roundcube:object name="plugin.recurrence_form" part="monthly" class="form-section" />
|
||||
</div>
|
||||
<div class="recurrence-form border-after" id="recurrence-form-yearly">
|
||||
<roundcube:object name="plugin.recurrence_form" part="yearly" class="form-section" />
|
||||
</div>
|
||||
<div class="recurrence-form" id="recurrence-form-until">
|
||||
<roundcube:object name="plugin.recurrence_form" part="until" class="form-section" />
|
||||
</div>
|
||||
<div class="recurrence-form" id="recurrence-form-rdate">
|
||||
<roundcube:object name="plugin.recurrence_form" part="rdate" class="form-section" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- attachments list (with upload form) -->
|
||||
<div id="taskedit-tab-2">
|
||||
<div id="taskedit-panel-attachments">
|
||||
<div id="taskedit-attachments">
|
||||
<roundcube:object name="plugin.attachments_list" id="taskedit-attachment-list" class="attachmentslist" />
|
||||
</div>
|
||||
|
|
|
@ -385,8 +385,9 @@ function rcube_tasklist_ui(settings)
|
|||
completeness_slider.slider('value', parseInt(this.value))
|
||||
});
|
||||
|
||||
// register events on alarm fields
|
||||
// register events on alarms and recurrence fields
|
||||
me.init_alarms_edit('#taskedit-alarms');
|
||||
me.init_recurrence_edit('#eventedit');
|
||||
|
||||
$('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings);
|
||||
|
||||
|
@ -1160,13 +1161,20 @@ function rcube_tasklist_ui(settings)
|
|||
});
|
||||
}
|
||||
|
||||
if (rec.recurrence && rec.recurrence_text) {
|
||||
$('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
|
||||
}
|
||||
else {
|
||||
$('#task-recurrence').hide();
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -1268,6 +1276,9 @@ function rcube_tasklist_ui(settings)
|
|||
// set alarm(s)
|
||||
me.set_alarms_edit('#taskedit-alarms', action != 'new' && rec.valarms ? rec.valarms : []);
|
||||
|
||||
// set recurrence
|
||||
me.set_recurrence_edit(rec);
|
||||
|
||||
// attachments
|
||||
rcmail.enable_command('remove-attachment', list.editable);
|
||||
me.selected_task.deleted_attachments = [];
|
||||
|
@ -1298,6 +1309,7 @@ function rcube_tasklist_ui(settings)
|
|||
me.selected_task.tags = [];
|
||||
me.selected_task.attachments = [];
|
||||
me.selected_task.valarms = me.serialize_alarms('#taskedit-alarms');
|
||||
me.selected_task.recurrence = me.serialize_recurrence(rectime.val());
|
||||
|
||||
// do some basic input validation
|
||||
if (!me.selected_task.title || !me.selected_task.title.length) {
|
||||
|
|
|
@ -197,10 +197,16 @@ class tasklist extends rcube_plugin
|
|||
|
||||
case 'edit':
|
||||
$rec = $this->prepare_task($rec);
|
||||
$clone = $this->handle_recurrence($rec, $this->driver->get_task($rec));
|
||||
if ($success = $this->driver->edit_task($rec)) {
|
||||
$refresh[] = $this->driver->get_task($rec);
|
||||
$this->cleanup_task($rec);
|
||||
|
||||
// add clone from recurring task
|
||||
if ($clone && $this->driver->create_task($clone)) {
|
||||
$refresh[] = $this->driver->get_task($clone);
|
||||
}
|
||||
|
||||
// move all childs if list assignment was changed
|
||||
if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) {
|
||||
foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) {
|
||||
|
@ -419,6 +425,36 @@ class tasklist extends rcube_plugin
|
|||
$rec['valarms'] = $valarms;
|
||||
}
|
||||
|
||||
// convert the submitted recurrence settings
|
||||
if (is_array($rec['recurrence'])) {
|
||||
$refdate = null;
|
||||
if (!empty($rec['date'])) {
|
||||
$refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
|
||||
}
|
||||
else if (!empty($rec['startdate'])) {
|
||||
$refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
|
||||
}
|
||||
|
||||
if ($refdate) {
|
||||
$rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate);
|
||||
|
||||
// translate count into an absolute end date.
|
||||
// why? because when shifting completed tasks to the next recurrence,
|
||||
// the initial start date to count from gets lost.
|
||||
if ($rec['recurrence']['COUNT']) {
|
||||
$engine = libcalendaring::get_recurrence();
|
||||
$engine->init($rec['recurrence'], $refdate);
|
||||
if ($until = $engine->end()) {
|
||||
$rec['recurrence']['UNTIL'] = $until;
|
||||
unset($rec['recurrence']['COUNT']);
|
||||
}
|
||||
}
|
||||
}
|
||||
else { // recurrence requires a reference date
|
||||
$rec['recurrence'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$attachments = array();
|
||||
$taskid = $rec['id'];
|
||||
if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
|
||||
|
@ -480,6 +516,62 @@ class tasklist extends rcube_plugin
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When flagging a recurring task as complete,
|
||||
* clone it and shift dates to the next occurrence
|
||||
*/
|
||||
private function handle_recurrence(&$rec, $old)
|
||||
{
|
||||
$clone = null;
|
||||
if ($rec['complete'] == 1.0 && $old && $old['complete'] < 1.0 && is_array($rec['recurrence'])) {
|
||||
$engine = libcalendaring::get_recurrence();
|
||||
$rrule = $rec['recurrence'];
|
||||
$engine->init($rrule);
|
||||
$updates = array();
|
||||
|
||||
// compute the next occurrence of date attributes
|
||||
foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) {
|
||||
$date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
|
||||
$engine->set_start($date);
|
||||
if ($next = $engine->next()) {
|
||||
$updates[$date_key] = $next->format('Y-m-d');
|
||||
if (!empty($rec[$time_key]))
|
||||
$updates[$time_key] = $next->format('H:i');
|
||||
}
|
||||
}
|
||||
|
||||
// shift absolute alarm dates
|
||||
if (!empty($updates) && is_array($rec['valarms'])) {
|
||||
$updates['valarms'] = array();
|
||||
unset($rrule['UNTIL'], $rrule['COUNT']); // make recurrence rule unlimited
|
||||
$engine->init($rrule);
|
||||
|
||||
foreach ($rec['valarms'] as $i => $alarm) {
|
||||
if ($alarm['trigger'] instanceof DateTime) {
|
||||
$engine->set_start($alarm['trigger']);
|
||||
if ($next = $engine->next()) {
|
||||
$alarm['trigger'] = $next;
|
||||
}
|
||||
}
|
||||
$updates['valarms'][$i] = $alarm;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($updates)) {
|
||||
// clone task to save a completed copy
|
||||
$clone = $rec;
|
||||
$clone['uid'] = $this->generate_uid();
|
||||
$clone['parent_id'] = $rec['id'];
|
||||
unset($clone['id'], $clone['recurrence'], $clone['attachments']);
|
||||
|
||||
// update the task but unset completed flag
|
||||
$rec = array_merge($rec, $updates);
|
||||
$rec['complete'] = $old['complete'];
|
||||
}
|
||||
}
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatcher for tasklist actions initiated by the client
|
||||
|
@ -677,6 +769,11 @@ class tasklist extends rcube_plugin
|
|||
$rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
|
||||
}
|
||||
|
||||
if ($rec['recurrence']) {
|
||||
$rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']);
|
||||
$rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']);
|
||||
}
|
||||
|
||||
foreach ((array)$rec['attachments'] as $k => $attachment) {
|
||||
$rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ class tasklist_ui
|
|||
$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.recurrence_form', array($this->plugin->lib, 'recurrence_form'));
|
||||
$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'));
|
||||
|
|
Loading…
Add table
Reference in a new issue