Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F4682964
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
152 KB
Referenced Files
None
Subscribers
None
View Options
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
@@ -1,498 +1,600 @@
<?php
/**
* Kolab Groupware driver for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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;
private $plugin;
private $lists;
private $folders = array();
private $tasks = array();
/**
* Default constructor
*/
public function __construct($plugin)
{
$this->rc = $plugin->rc;
$this->plugin = $plugin;
$this->_read_lists();
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_lists()
{
// already read sources
if (isset($this->lists))
return $this->lists;
// get all folders that have type "task"
$this->folders = kolab_storage::get_folders('task');
$this->lists = array();
// convert to UTF8 and sort
$names = array();
foreach ($this->folders as $i => $folder) {
$names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
$this->folders[$folder->name] = $folder;
}
asort($names, SORT_LOCALE_STRING);
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
$listnames = array();
foreach ($names as $utf7name => $name) {
$folder = $this->folders[$utf7name];
$path_imap = explode($delim, $name);
$editname = array_pop($path_imap); // pop off raw name part
$path_imap = join($delim, $path_imap);
$name = kolab_storage::folder_displayname(kolab_storage::object_name($utf7name), $listnames);
$tasklist = array(
'id' => kolab_storage::folder_id($utf7name),
'name' => $name,
'editname' => $editname,
'color' => 'CC0000',
'showalarms' => false,
'editable' => true,
'active' => $folder->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION),
'parentfolder' => $path_imap,
);
$this->lists[$tasklist['id']] = $tasklist;
$this->folders[$tasklist['id']] = $folder;
}
}
/**
* Get a list of available task lists from this source
*/
public function get_lists()
{
// attempt to create a default list for this user
if (empty($this->lists)) {
if ($this->create_list(array('name' => 'Default', 'color' => '000000')))
$this->_read_lists();
}
return $this->lists;
}
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled
* @return mixed ID of the new list on success, False on error
*/
public function create_list($prop)
{
$prop['type'] = 'task';
$prop['subscribed'] = kolab_storage::SERVERSIDE_SUBSCRIPTION; // subscribe to folder by default
$folder = kolab_storage::folder_update($prop);
if ($folder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
return kolab_storage::folder_id($folder);
}
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* id: List Identifier
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
public function edit_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
$prop['oldname'] = $folder->name;
$prop['type'] = 'task';
$newfolder = kolab_storage::folder_update($prop);
if ($newfolder === false) {
$this->last_error = kolab_storage::$last_error;
return false;
}
// create ID
return kolab_storage::folder_id($newfolder);
}
return false;
}
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* @return boolean True on success, Fales on failure
*/
public function subscribe_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
return $folder->subscribe($prop['active'], kolab_storage::SERVERSIDE_SUBSCRIPTION);
}
return false;
}
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* id: list Identifier
* @return boolean True on success, Fales on failure
*/
public function remove_list($prop)
{
if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
if (kolab_storage::folder_delete($folder->name))
return true;
else
$this->last_error = kolab_storage::$last_error;
}
return false;
}
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
*/
public function count_tasks($lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
$today_date = new DateTime('now', $this->plugin->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
foreach ($lists as $list_id) {
$folder = $this->folders[$list_id];
foreach ((array)$folder->select(array(array('tags','!~','x-complete'))) as $record) {
$rec = $this->_to_rcube_task($record);
if ($rec['complete'] >= 1.0) // don't count complete tasks
continue;
$counts['all']++;
if ($rec['flagged'])
$counts['flagged']++;
if (empty($rec['date']))
$counts['nodate']++;
else if ($rec['date'] == $today)
$counts['today']++;
else if ($rec['date'] == $tomorrow)
$counts['tomorrow']++;
else if ($rec['date'] < $today)
$counts['overdue']++;
}
}
return $counts;
}
/**
* Get all taks records matching the given filter
*
* @param array Hash array with filter criterias:
* - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
public function list_tasks($filter, $lists = null)
{
if (empty($lists))
$lists = array_keys($this->lists);
else if (is_string($lists))
$lists = explode(',', $lists);
$results = array();
// query Kolab storage
$query = array();
if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
$query[] = array('tags','~','x-complete');
else
$query[] = array('tags','!~','x-complete');
// full text search (only works with cache enabled)
if ($filter['search']) {
$search = mb_strtolower($filter['search']);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = array('words', '~', $word);
}
}
foreach ($lists as $list_id) {
$folder = $this->folders[$list_id];
foreach ((array)$folder->select($query) as $record) {
$task = $this->_to_rcube_task($record);
$task['list'] = $list_id;
// TODO: post-filter tasks returned from storage
$results[] = $task;
}
}
return $results;
}
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
public function get_task($prop)
{
$id = is_array($prop) ? $prop['uid'] : $prop;
$list_id = is_array($prop) ? $prop['list'] : null;
$folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
// find task in the available folders
foreach ($folders as $folder) {
if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
$this->tasks[$id] = $this->_to_rcube_task($object);
break;
}
}
return $this->tasks[$id];
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_task($record)
{
$task = array(
'id' => $record['uid'],
'uid' => $record['uid'],
'title' => $record['title'],
# 'location' => $record['location'],
'description' => $record['description'],
'tags' => (array)$record['categories'],
'flagged' => $record['priority'] == 1,
'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100),
'parent_id' => $record['parent_id'],
);
// convert from DateTime to internal date format
if (is_a($record['due'], 'DateTime')) {
$task['date'] = $record['due']->format('Y-m-d');
$task['time'] = $record['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');
$task['starttime'] = $record['start']->format('h:i');
}
if (is_a($record['dtstamp'], 'DateTime')) {
$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;
}
/**
* Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
* (opposite of self::_to_rcube_event())
*/
private function _from_rcube_task($task, $old = array())
{
$object = $task;
$object['categories'] = (array)$task['tags'];
if (!empty($task['date'])) {
$object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone);
if (empty($task['time']))
$object['due']->_dateonly = true;
unset($object['date']);
}
if (!empty($task['startdate'])) {
$object['start'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
if (empty($task['starttime']))
$object['start']->_dateonly = true;
unset($object['startdate']);
}
$object['complete'] = $task['complete'] * 100;
if ($task['complete'] == 1.0)
$object['status'] = 'COMPLETED';
if ($task['flagged'])
$object['priority'] = 1;
else
$object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0;
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($object[$key]) && $key[0] == '_')
$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;
}
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return mixed New task ID on success, False on error
*/
public function create_task($task)
{
return $this->edit_task($task);
}
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties (see header of tasklist_driver.php)
* @return boolean True on success, False on error
*/
public function edit_task($task)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
// moved from another folder
if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
if (!$fromfolder->move($task['uid'], $folder->name))
return false;
unset($task['_fromlist']);
}
// load previous version of this task to merge
if ($task['id']) {
$old = $folder->get_object($task['uid']);
if (!$old || PEAR::isError($old))
return false;
}
// generate new task object from RC input
$object = $this->_from_rcube_task($task, $old);
$saved = $folder->save($object, 'task', $task['id']);
if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving task object to Kolab server"),
true, false);
$saved = false;
}
else {
- $task['id'] = $task['uid'];
+ $task = $this->_to_rcube_task($object);
+ $task['list'] = $list_id;
$this->tasks[$task['uid']] = $task;
}
return $saved;
}
/**
* Remove a single task from the database
*
* @param array Hash array with task properties:
* id: Task identifier
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
public function delete_task($task, $force = true)
{
$list_id = $task['list'];
if (!$list_id || !($folder = $this->folders[$list_id]))
return false;
return $folder->delete($task['uid']);
}
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties:
* id: Task identifier
* @return boolean True on success, False on error
*/
public function undelete_task($prop)
{
// TODO: implement this
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)
+ {
+ $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)
{
$select = kolab_storage::folder_selector('task', array('name' => 'parent', 'id' => 'edit-parentfolder'), null);
$formfields['parent'] = array(
'id' => 'edit-parentfolder',
'label' => $this->plugin->gettext('parentfolder'),
'value' => $select->show(''),
);
return parent::tasklist_edit_form($formfields);
}
}
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
@@ -1,206 +1,234 @@
<?php
/**
* Driver interface for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Struct of an internal task object how it is passed from/to the driver classes:
*
* $task = array(
* 'id' => 'Task ID used for editing', // must be unique for the current user
* 'parent_id' => 'ID of parent task', // null if top-level task
* 'uid' => 'Unique identifier of this task',
* 'list' => 'Task list identifier to add the task to or where the task is stored',
* 'changed' => <DateTime>, // Last modification date/time of the record
* 'title' => 'Event title/summary',
* 'description' => 'Event description',
* 'tags' => array(), // List of tags for this task
* 'date' => 'Due date', // as string of format YYYY-MM-DD or null if no date is set
* 'time' => 'Due time', // as string of format hh::ii or null if no due time is set
* 'startdate' => 'Start date' // Delay start of the task until that date
* 'starttime' => 'Start time' // ...and time
* 'categories' => 'Task category',
* 'flagged' => 'Boolean value whether this record is flagged',
* 'complete' => 'Float value representing the completeness state (range 0..1)',
* 'sensitivity' => 0|1|2, // Event sensitivity (0=public, 1=private, 2=confidential)
* 'alarms' => '-15M:DISPLAY', // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before due time)
* '_fromlist' => 'List identifier where the task was stored before',
* );
*/
/**
* Driver interface for the Tasklist plugin
*/
abstract class tasklist_driver
{
// features supported by the backend
public $alarms = false;
public $attachments = false;
public $undelete = false; // task undelete action
public $sortable = false;
public $alarm_types = array('DISPLAY');
public $last_error;
/**
* Get a list of available task lists from this source
*/
abstract function get_lists();
/**
* Create a new list assigned to the current user
*
* @param array Hash array with list properties
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled
* @return mixed ID of the new list on success, False on error
*/
abstract function create_list($prop);
/**
* Update properties of an existing tasklist
*
* @param array Hash array with list properties
* id: List Identifier
* name: List name
* color: The color of the list
* showalarms: True if alarms are enabled (if supported)
* @return boolean True on success, Fales on failure
*/
abstract function edit_list($prop);
/**
* Set active/subscribed state of a list
*
* @param array Hash array with list properties
* id: List Identifier
* active: True if list is active, false if not
* @return boolean True on success, Fales on failure
*/
abstract function subscribe_list($prop);
/**
* Delete the given list with all its contents
*
* @param array Hash array with list properties
* id: list Identifier
* @return boolean True on success, Fales on failure
*/
abstract function remove_list($prop);
/**
* Get number of tasks matching the given filter
*
* @param array List of lists to count tasks of
* @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
*/
abstract function count_tasks($lists = null);
/**
* Get all taks records matching the given filter
*
* @param array Hash array with filter criterias:
* - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
* - from: Date range start as string (Y-m-d)
* - to: Date range end as string (Y-m-d)
* - search: Search query string
* @param array List of lists to get tasks from
* @return array List of tasks records matchin the criteria
*/
abstract function list_tasks($filter, $lists = null);
/**
* Return data of a specific task
*
* @param mixed Hash array with task properties or task UID
* @return array Hash array with task properties or false if not found
*/
abstract public function get_task($prop);
/**
* Add a single task to the database
*
* @param array Hash array with task properties (see header of this file)
* @return mixed New event ID on success, False on error
*/
abstract function create_task($prop);
/**
* Update an task entry with the given data
*
* @param array Hash array with task properties (see header of this file)
* @return boolean True on success, False on error
*/
abstract function edit_task($prop);
/**
* Remove a single task from the database
*
* @param array Hash array with task properties:
* id: Task identifier
* @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
* @return boolean True on success, False on error
*/
abstract function delete_task($prop, $force = true);
/**
* Restores a single deleted task (if supported)
*
* @param array Hash array with task properties:
* id: Task identifier
* @return boolean True on success, False on error
*/
public function undelete_task($prop)
{
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
*/
public function list_categories()
{
$rcmail = rcmail::get_instance();
return $rcmail->config->get('tasklist_categories', array());
}
/**
* Build the edit/create form for lists.
* This gives the drivers the opportunity to add more list properties
*
* @param array List with form fields to be rendered
* @return string HTML content of the form
*/
public function tasklist_edit_form($formfields)
{
$html = '';
foreach ($formfields as $prop => $field) {
$html .= html::div('form-section',
html::label($field['id'], $field['label']) .
$field['value']);
}
return $html;
}
}
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
@@ -1,56 +1,61 @@
<?php
$labels = array();
$labels['navtitle'] = 'Aufgaben';
$labels['lists'] = 'Aufgabenlisten';
$labels['list'] = 'Liste';
$labels['tags'] = 'Tags';
$labels['newtask'] = 'Neue Aufgabe';
$labels['createnewtask'] = 'Neue Aufgabe eingeben (z.B. Samstag, Rasenmähen)';
$labels['createfrommail'] = 'Als Aufgabe speichern';
$labels['mark'] = 'Markieren';
$labels['unmark'] = 'Markierung aufheben';
$labels['edit'] = 'Bearbeiten';
$labels['delete'] = 'Löschen';
$labels['title'] = 'Titel';
$labels['description'] = 'Beschreibung';
$labels['datetime'] = 'Datum/Zeit';
$labels['start'] = 'Beginn';
$labels['all'] = 'Alle';
$labels['flagged'] = 'Markiert';
$labels['complete'] = 'Erledigt';
$labels['overdue'] = 'Überfällig';
$labels['today'] = 'Heute';
$labels['tomorrow'] = 'Morgen';
$labels['next7days'] = 'Nächste 7 Tage';
$labels['later'] = 'Später';
$labels['nodate'] = 'kein Datum';
$labels['removetag'] = 'Löschen';
$labels['taskdetails'] = 'Details';
$labels['newtask'] = 'Neue Aufgabe';
$labels['edittask'] = 'Aufgabe bearbeiten';
$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...';
$labels['listname'] = 'Name';
$labels['showalarms'] = 'Erinnerungen anzeigen';
$labels['import'] = 'Importieren';
// date words
$labels['on'] = 'am';
$labels['at'] = 'um';
$labels['this'] = 'diesen';
$labels['next'] = 'nächsten';
// mesages
$labels['savingdata'] = 'Daten werden gespeichert...';
$labels['errorsaving'] = 'Fehler beim Speichern.';
$labels['notasksfound'] = 'Für die aktuellen Kriterien wurden keine Aufgaben gefunden.';
$labels['invalidstartduedates'] = 'Beginn der Aufgabe darf nicht grösser als das Enddatum sein.';
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
@@ -1,56 +1,61 @@
<?php
$labels = array();
$labels['navtitle'] = 'Tasks';
$labels['lists'] = 'Tasklists';
$labels['list'] = 'Tasklist';
$labels['tags'] = 'Tags';
$labels['newtask'] = 'New Task';
$labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)';
$labels['createfrommail'] = 'Save as task';
$labels['mark'] = 'Mark';
$labels['unmark'] = 'Unmark';
$labels['edit'] = 'Edit';
$labels['delete'] = 'Delete';
$labels['title'] = 'Title';
$labels['description'] = 'Description';
$labels['datetime'] = 'Date/Time';
$labels['start'] = 'Start';
$labels['all'] = 'All';
$labels['flagged'] = 'Flagged';
$labels['complete'] = 'Complete';
$labels['overdue'] = 'Overdue';
$labels['today'] = 'Today';
$labels['tomorrow'] = 'Tomorrow';
$labels['next7days'] = 'Next 7 days';
$labels['later'] = 'Later';
$labels['nodate'] = 'no date';
$labels['removetag'] = 'Remove';
$labels['taskdetails'] = 'Details';
$labels['newtask'] = 'New Task';
$labels['edittask'] = 'Edit Task';
$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...';
$labels['listname'] = 'Name';
$labels['showalarms'] = 'Show alarms';
$labels['import'] = 'Import';
// date words
$labels['on'] = 'on';
$labels['at'] = 'at';
$labels['this'] = 'this';
$labels['next'] = 'next';
// mesages
$labels['savingdata'] = 'Saving data...';
$labels['errorsaving'] = 'Failed to save data.';
$labels['notasksfound'] = 'No tasks found for the given criteria';
$labels['invalidstartduedates'] = 'Start date must not be greater than due date.';
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
@@ -1,675 +1,748 @@
/**
* Roundcube Taklist plugin styles for skin "Larry"
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* $Id$
*/
#taskbar a.button-tasklist span.button-inner {
background-image: url(buttons.png);
background-position: 0 0;
}
#taskbar a.button-tasklist:hover span.button-inner,
#taskbar a.button-tasklist.button-selected span.button-inner {
background-position: 0 -26px;
}
ul.toolbarmenu li span.icon.taskadd {
background-image: url(buttons.png);
background-position: -4px -90px;
}
#sidebar {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 240px;
}
.tasklistview #searchmenulink {
width: 15px;
}
#tagsbox {
position: absolute;
top: 42px;
left: 0;
width: 100%;
height: 242px;
}
#tasklistsbox {
position: absolute;
top: 300px;
left: 0;
width: 100%;
bottom: 0px;
}
#taskselector {
margin: -4px 0 0;
padding: 0;
}
#taskselector li {
display: inline-block;
position: relative;
font-size: 90%;
padding-right: 0.3em;
}
#tagslist li,
#taskselector li a {
display: inline-block;
color: #004458;
min-width: 4em;
padding: 0.2em 0.6em 0.3em 0.6em;
text-align: center;
text-decoration: none;
border: 1px solid #eee;
border-color: transparent;
}
#taskselector li:first-child {
border-top: 0;
border-radius: 4px 4px 0 0;
}
#taskselector li:last-child {
border-bottom: 0;
border-radius: 0 0 4px 4px;
}
#taskselector li.overdue a {
color: #b72a2a;
font-weight: bold;
}
#taskselector li.inactive a {
color: #97b3bf;
}
#tagslist li.selected,
#taskselector li.selected a {
color: #fff;
background: #005d76;
background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
background: linear-gradient(top, #005d76 0%, #004558 100%);
box-shadow: inset 0 1px 1px 0 #003645;
-o-box-shadow: inset 0 1px 1px 0 #003645;
-webkit-box-shadow: inset 0 1px 1px 0 #003645;
-moz-box-shadow: inset 0 1px 1px 0 #003645;
border-color: #003645;
border-radius: 9px;
text-shadow: none;
}
#taskselector li .count {
display: none;
position: absolute;
top: -18px;
right: 5px;
min-width: 1.8em;
padding: 2px 4px;
background: #004558;
background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
background: linear-gradient(top, #005d76 0%, #004558 100%);
box-shadow: 0 1px 2px 0 rgba(24,24,24,0.6);
color: #fff;
border-radius: 3px;
text-align: center;
font-weight: bold;
font-size: 80%;
text-shadow: none;
}
#taskselector li .count:after {
content: "";
position: absolute;
bottom: -5px;
left: 50%;
margin-left: -5px;
border-style: solid;
border-width: 5px 5px 0;
border-color: #004558 transparent;
/* reduce the damage in FF3.0 */
display: block;
width: 0;
}
#taskselector li.overdue .count {
background: #ff3800;
}
#taskselector li.overdue .count:after {
border-color: #ff3800 transparent;
}
#tagslist {
padding: 0;
margin: 6px;
list-style: none;
}
#tagslist li {
display: inline-block;
color: #004458;
margin-right: 0.5em;
margin-bottom: 0.4em;
min-width: 1.2em;
cursor: pointer;
}
#tasklists li {
margin: 0;
height: 20px;
padding: 6px 8px 2px;
display: block;
position: relative;
white-space: nowrap;
}
#tasklists li label {
display: block;
}
#tasklists li span.listname {
cursor: default;
padding-bottom: 2px;
color: #004458;
}
#tasklists li span.handle {
display: none;
}
#tasklists li input {
position: absolute;
top: 5px;
right: 5px;
}
#mainview-right {
position: absolute;
top: 0;
left: 256px;
right: 0;
bottom: 0;
}
#taskstoolbar {
position: absolute;
top: -6px;
left: 0;
width: 100%;
height: 40px;
white-space: nowrap;
}
#taskstoolbar a.button.newtask {
background-image: url(buttons.png);
background-position: center -53px;
}
.tasklistview #quicksearchbar {
top: -7px;
}
#quickaddbox {
position: absolute;
top: 0;
left: 0;
width: 60%;
height: 32px;
white-space: nowrap;
}
#quickaddinput {
width: 85%;
margin: 0;
padding: 3px 8px;
height: 18px;
background: #f1f1f1;
background: rgba(255, 255, 255, 0.7);
border-color: #a3a3a3;
font-weight: bold;
}
#quickaddbox .button {
margin-left: 5px;
padding: 3px 10px;
font-weight: bold;
}
#tasksview {
position: absolute;
top: 42px;
left: 0;
right: 0;
bottom: 0;
padding-bottom: 28px;
background: rgba(255, 255, 255, 0.2);
}
#message.statusbar {
border-top: 1px solid #c3c3c3;
}
#tasksview .scroller {
position: absolute;
left: 0;
top: 35px;
width: 100%;
bottom: 28px;
overflow: auto;
}
#tasksview .buttonbar {
color: #777;
background: #eee;
background: -moz-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eee), color-stop(100%,#dfdfdf));
background: -o-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: -ms-linear-gradient(top, #eee 0%, #dfdfdf 100%);
background: linear-gradient(top, #eee 0%, #dfdfdf 100%);
border-bottom: 1px solid #ccc;
}
#thelist {
padding: 0;
margin: 1em;
list-style: none;
}
#listmessagebox {
display: none;
font-size: 14px;
color: #666;
margin: 1.5em;
text-shadow: 0px 1px 1px #fff;
text-align:center;
}
.taskitem {
display: block;
margin-bottom: 5px;
}
.taskitem.dragging {
opacity: 0.5;
}
.taskitem .childtasks {
padding: 0;
margin: 0.5em 0 0 2em;
list-style: none;
}
.taskhead {
position: relative;
padding: 4px 5px 3px 5px;
border: 1px solid #fff;
border-radius: 5px;
background: #fff;
-webkit-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
-moz-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
padding-right: 26em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
}
.taskhead.droptarget {
border-color: #4787b1;
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);
}
.taskhead .complete {
margin: -1px 1em 0 0;
}
.taskhead .title {
font-size: 12px;
}
.taskhead .flagged {
display: inline-block;
visibility: hidden;
width: 16px;
height: 16px;
background: url(sprites.png) -2px -3px no-repeat;
margin: -3px 1em 0 0;
vertical-align: middle;
cursor: pointer;
}
.taskhead:hover .flagged {
visibility: visible;
}
.taskhead.flagged .flagged {
visibility: visible;
background-position: -2px -23px;
}
.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;
}
.taskhead .tags .tag {
font-size: 85%;
background: #d9ecf4;
border: 1px solid #c2dae5;
border-radius: 4px;
- padding: 2px 8px;
+ padding: 1px 7px;
margin-right: 3px;
}
.taskhead .date {
position: absolute;
top: 6px;
right: 30px;
text-align: right;
cursor: pointer;
}
.taskhead.nodate .date {
color: #ddd;
}
.taskhead.overdue .date {
color: #d00;
}
.taskhead.nodate:hover .date {
color: #999;
}
.taskhead .date input {
padding: 1px 2px;
border: 1px solid #ddd;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
outline: none;
text-align: right;
}
.taskhead .actions,
.taskhead .delete {
display: block;
visibility: hidden;
position: absolute;
top: 3px;
right: 6px;
width: 18px;
height: 18px;
background: url(sprites.png) 0 -80px no-repeat;
text-indent: -1000px;
overflow: hidden;
cursor: pointer;
}
.taskhead .delete {
background-position: 0 -40px;
}
.taskhead:hover .actions,
.taskhead:hover .delete {
visibility: visible;
}
.taskhead.complete {
opacity: 0.6;
}
.taskhead.complete .title {
text-decoration: line-through;
}
.taskhead .progressbar {
position: absolute;
bottom: 1px;
left: 6px;
right: 6px;
height: 2px;
}
.taskhead.complete .progressbar {
display: none;
}
.taskhead .progressvalue {
height: 1px;
background: rgba(1, 124, 180, 0.2);
border-top: 1px solid #219de6;
}
ul.toolbarmenu li span.add {
background-image: url(sprites.png);
background-position: 0 -100px;
}
ul.toolbarmenu li span.delete {
background-position: 0 -1508px;
}
.taskitem-draghelper {
/*
width: 32px;
height: 26px;
*/
background: #444;
border: 1px solid #555;
border-radius: 4px;
box-shadow: 0 2px 6px 0 #333;
-moz-box-shadow: 0 2px 6px 0 #333;
-webkit-box-shadow: 0 2px 6px 0 #333;
-o-box-shadow: 0 2px 6px 0 #333;
z-index: 5000;
padding: 2px 10px;
font-size: 20px;
color: #ccc;
opacity: 0.92;
filter: alpha(opacity=92);
text-shadow: 0px 1px 1px #333;
}
#rootdroppable {
display: none;
position: absolute;
top: 36px;
left: 1em;
right: 1em;
height: 5px;
background: #ddd;
border-radius: 3px;
}
#rootdroppable.droptarget {
background: #4787b1;
box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-moz-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-webkit-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
-o-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
}
/*** task edit form ***/
#taskedit,
#taskshow {
display:none;
}
+#taskedit {
+ position: relative;
+ top: -1.5em;
+ padding: 0.5em 0.1em;
+ margin: 0 -0.2em;
+}
+
#taskshow h2 {
margin-top: -0.5em;
}
#taskshow label {
color: #999;
}
#task-parent-title {
position: relative;
top: -0.6em;
}
a.morelink {
font-size: 90%;
color: #0069a6;
text-decoration: none;
outline: none;
}
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;
margin-bottom: 0.8em;
}
.form-section label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
+ margin-bottom: 0.3em;
}
label.block {
display: block;
margin-bottom: 0.3em;
}
#edit-completeness-slider {
display: inline-block;
margin-left: 2em;
width: 30em;
height: 0.8em;
border: 1px solid #ccc;
}
#edit-tagline {
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
*/
.tagedit-list {
width: 100%;
margin: 0;
padding: 4px 4px 0 5px;
overflow: auto;
min-height: 26px;
background: #fff;
border: 1px solid #b2b2b2;
border-radius: 4px;
box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-moz-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-webkit-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
-o-box-shadow: inset 0 0 2px 1px rgba(0,0,0, 0.1);
}
.tagedit-list li.tagedit-listelement {
list-style-type: none;
float: left;
margin: 0 4px 4px 0;
padding: 0;
}
/* New Item input */
.tagedit-list li.tagedit-listelement-new input {
border: 0;
height: 100%;
padding: 4px 1px;
width: 15px;
background: #fff;
border-radius: 0;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
}
.tagedit-list li.tagedit-listelement-new input:focus {
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
-o-box-shadow: none;
outline: none;
}
.tagedit-list li.tagedit-listelement-new input.tagedit-input-disabled {
display: none;
}
/* Item that is put to the List */
.form-section span.tag-element,
.tagedit-list li.tagedit-listelement-old {
padding: 3px 0 1px 6px;
background: #ddeef5;
background: -moz-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#edf6fa), color-stop(100%,#d6e9f3));
background: -o-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: -ms-linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
background: linear-gradient(top, #edf6fa 0%, #d6e9f3 100%);
border: 1px solid #c2dae5;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;
color: #0d5165;
}
.form-section span.tag-element {
margin-right: 0.6em;
padding: 2px 6px;
/* cursor: pointer; */
}
.tagedit-list li.tagedit-listelement-old a.tagedit-close,
.tagedit-list li.tagedit-listelement-old a.tagedit-break,
.tagedit-list li.tagedit-listelement-old a.tagedit-delete,
.tagedit-list li.tagedit-listelement-old a.tagedit-save {
text-indent: -2000px;
display: inline-block;
position: relative;
top: -1px;
width: 16px;
height: 16px;
margin: 0 2px 0 6px;
background: url(sprites.png) -2px -122px no-repeat;
cursor: pointer;
}
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="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 114b7ec5..773badf7 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -1,144 +1,148 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="tasklistview noscroll">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
<div id="sidebar">
<div id="taskstoolbar" class="toolbar">
<roundcube:button command="newtask" type="link" class="button newtask disabled" classAct="button newtask" classSel="button newtask pressed" label="tasklist.newtask" title="tasklist.newtask" />
<roundcube:container name="toolbar" id="taskstoolbar" />
</div>
<div id="tagsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="tasklist.tags" id="taglist" /></h2>
<div class="scroller">
<roundcube:object name="plugin.tagslist" id="tagslist" />
</div>
</div>
<div id="tasklistsbox" class="uibox listbox">
<h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
<div class="scroller withfooter">
<roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
</div>
<div class="boxfooter">
<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
</div>
</div>
</div>
<div id="mainview-right">
<div id="quickaddbox">
<roundcube:object name="plugin.quickaddform" />
</div>
<div id="quicksearchbar">
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
<a id="searchmenulink" class="iconbutton searchoptions" > </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
<div id="tasksview" class="uibox">
<div class="boxtitle buttonbar">
<ul id="taskselector">
<li class="all selected"><a href="#all"><roundcube:label name="tasklist.all" /><span class="count"></span></a></li>
<li class="overdue inactive"><a href="#overdue"><roundcube:label name="tasklist.overdue" /><span class="count"></span></a></li>
<li class="flagged"><a href="#flagged"><roundcube:label name="tasklist.flagged" /><span class="count"></span></a></li>
<li class="today"><a href="#today"><roundcube:label name="tasklist.today" /><span class="count"></span></a></li>
<li class="tomorrow"><a href="#tomorrow"><roundcube:label name="tasklist.tomorrow" /><span class="count"></span></a></li>
<li class="week"><a href="#week"><roundcube:label name="tasklist.next7days" /></a></li>
<li class="later"><a href="#later"><roundcube:label name="tasklist.later" /></a></li>
<li class="nodate"><a href="#nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li>
<li class="complete"><a href="#complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li>
</ul>
</div>
<div class="scroller">
<roundcube:object name="plugin.tasks" id="thelist" />
<div id="listmessagebox"></div>
</div>
<div id="rootdroppable"></div>
<roundcube:object name="message" id="message" class="statusbar" />
</div>
</div>
</div>
<div id="taskitemmenu" class="popupmenu">
<ul class="toolbarmenu iconized">
<li><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
<li><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
<li><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
</ul>
</div>
<div id="tasklistoptionsmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="list-edit" label="edit" classAct="active" /></li>
<li><roundcube:button command="list-remove" label="delete" classAct="active" /></li>
<li><roundcube:button command="list-import" label="tasklist.import" classAct="active" /></li>
<roundcube:if condition="env:tasklist_driver == 'kolab'" />
<li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:endif />
</ul>
</div>
<div id="taskshow">
<div class="form-section" id="task-parent-title"></div>
<div class="form-section">
<h2 id="task-title"></h2>
</div>
<div id="task-description" class="form-section">
</div>
<div id="task-tags" class="form-section">
<label><roundcube:label name="tasklist.tags" /></label>
<span class="task-text"></span>
</div>
<div id="task-date" class="form-section">
<label><roundcube:label name="tasklist.datetime" /></label>
<span class="task-text"></span>
<span id="task-time"></span>
</div>
<div id="task-start" class="form-section">
<label><roundcube:label name="tasklist.start" /></label>
<span class="task-text"></span>
<span id="task-starttime"></span>
</div>
<div id="task-list" class="form-section">
<label><roundcube:label name="tasklist.list" /></label>
<span class="task-text"></span>
</div>
<div id="task-completeness" class="form-section">
<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" />
<div id="tasklistform" class="uidialog">
<roundcube:object name="plugin.tasklist_editform" />
</div>
<script type="text/javascript">
// UI startup
var UI = new rcube_mail_ui();
$(document).ready(function(e){
UI.init();
});
</script>
</body>
</html>
\ No newline at end of file
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 @@
-<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 39636d6f..e5a874aa 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -1,1270 +1,1401 @@
/**
* Client scripts for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
function rcube_tasklist_ui(settings)
{
/* constants */
var FILTER_MASK_ALL = 0;
var FILTER_MASK_TODAY = 1;
var FILTER_MASK_TOMORROW = 2;
var FILTER_MASK_WEEK = 4;
var FILTER_MASK_LATER = 8;
var FILTER_MASK_NODATE = 16;
var FILTER_MASK_OVERDUE = 32;
var FILTER_MASK_FLAGGED = 64;
var FILTER_MASK_COMPLETE = 128;
var filter_masks = {
all: FILTER_MASK_ALL,
today: FILTER_MASK_TODAY,
tomorrow: FILTER_MASK_TOMORROW,
week: FILTER_MASK_WEEK,
later: FILTER_MASK_LATER,
nodate: FILTER_MASK_NODATE,
overdue: FILTER_MASK_OVERDUE,
flagged: FILTER_MASK_FLAGGED,
complete: FILTER_MASK_COMPLETE
};
/* private vars */
var selector = 'all';
var tagsfilter = [];
var filtermask = FILTER_MASK_ALL;
var loadstate = { filter:-1, lists:'', search:null };
var idcount = 0;
var saving_lock;
var ui_loading;
var taskcounts = {};
var listindex = [];
var listdata = {};
var tags = [];
var draghelper;
var search_request;
var search_query;
var me = this;
// general datepicker settings
var datepicker_settings = {
// translate from PHP format to datepicker format
dateFormat: settings['date_format'].replace(/m/, 'mm').replace(/n/g, 'm').replace(/F/, 'MM').replace(/l/, 'DD').replace(/dd/, 'D').replace(/d/, 'dd').replace(/j/, 'd').replace(/Y/g, 'yy'),
firstDay : settings['first_day'],
// dayNamesMin: settings['days_short'],
// monthNames: settings['months'],
// monthNamesShort: settings['months'],
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true
};
var extended_datepicker_settings;
/* public members */
this.tasklists = rcmail.env.tasklists;
this.selected_task;
this.selected_list;
/* public methods */
this.init = init;
this.edit_task = task_edit_dialog;
this.delete_task = delete_task;
this.add_childtask = add_childtask;
this.quicksearch = quicksearch;
this.reset_search = reset_search;
this.list_remove = list_remove;
this.list_edit_dialog = list_edit_dialog;
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
*/
function init()
{
// sinitialize task list selectors
for (var id in me.tasklists) {
if ((li = rcmail.get_folder_li(id, 'rcmlitasklist'))) {
init_tasklist_li(li, id);
}
if (!me.tasklists.readonly && !me.selected_list) {
me.selected_list = id;
rcmail.enable_command('addtask', true);
$(li).click();
}
}
// register server callbacks
rcmail.addEventListener('plugin.data_ready', data_ready);
rcmail.addEventListener('plugin.refresh_task', update_taskitem);
rcmail.addEventListener('plugin.update_counts', update_counts);
rcmail.addEventListener('plugin.insert_tasklist', insert_list);
rcmail.addEventListener('plugin.update_tasklist', update_list);
rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null); });
rcmail.addEventListener('plugin.unlock_saving', unlock_saving);
// start loading tasks
fetch_counts();
list_tasks();
// register event handlers for UI elements
$('#taskselector a').click(function(e){
if (!$(this).parent().hasClass('inactive'))
list_tasks(this.href.replace(/^.*#/, ''));
return false;
});
// quick-add a task
$(rcmail.gui_objects.quickaddform).submit(function(e){
var tasktext = this.elements.text.value;
var rec = { id:-(++idcount), title:tasktext, readonly:true, mask:0, complete:0 };
save_task({ tempid:rec.id, raw:tasktext, list:me.selected_list }, 'new');
render_task(rec);
// clear form
this.reset();
return false;
});
// click-handler on tags list
$(rcmail.gui_objects.tagslist).click(function(e){
if (e.target.nodeName != 'LI')
return false;
var item = $(e.target),
tag = item.data('value');
// reset selection on regular clicks
var index = tagsfilter.indexOf(tag);
var shift = e.shiftKey || e.ctrlKey || e.metaKey;
if (!shift) {
if (tagsfilter.length > 1)
index = -1;
$('li', this).removeClass('selected');
tagsfilter = [];
}
// add tag to filter
if (index < 0) {
item.addClass('selected');
tagsfilter.push(tag);
}
else if (shift) {
item.removeClass('selected');
var a = tagsfilter.slice(0,index);
tagsfilter = a.concat(tagsfilter.slice(index+1));
}
list_tasks();
e.preventDefault();
return false;
})
.mousedown(function(e){
// disable content selection with the mouse
e.preventDefault();
return false;
});
// click-handler on task list items (delegate)
$(rcmail.gui_objects.resultlist).click(function(e){
var item = $(e.target);
if (!item.hasClass('taskhead'))
item = item.closest('div.taskhead');
// ignore
if (!item.length)
return;
var id = item.data('id'),
li = item.parent(),
rec = listdata[id];
switch (e.target.className) {
case 'complete':
rec.complete = e.target.checked ? 1 : 0;
li.toggleClass('complete');
save_task(rec, 'edit');
return true;
case 'flagged':
rec.flagged = rec.flagged ? 0 : 1;
li.toggleClass('flagged');
save_task(rec, 'edit');
break;
case 'date':
var link = $(e.target).html(''),
input = $('<input type="text" size="10" />').appendTo(link).val(rec.date || '')
input.datepicker($.extend({
onClose: function(dateText, inst) {
if (dateText != rec.date) {
rec.date = dateText;
save_task(rec, 'edit');
}
input.datepicker('destroy').remove();
link.html(dateText || rcmail.gettext('nodate','tasklist'));
},
}, extended_datepicker_settings)
)
.datepicker('setDate', rec.date)
.datepicker('show');
break;
case 'delete':
delete_task(id);
break;
case 'actions':
var pos, ref = $(e.target),
menu = $('#taskitemmenu');
if (menu.is(':visible') && menu.data('refid') == id) {
menu.hide();
}
else {
pos = ref.offset();
pos.top += ref.outerHeight();
pos.left += ref.width() - menu.outerWidth();
menu.css({ top:pos.top+'px', left:pos.left+'px' }).show();
menu.data('refid', id);
me.selected_task = rec;
}
e.bubble = false;
break;
default:
if (e.target.nodeName != 'INPUT')
task_show_dialog(id);
break;
}
return false;
})
.dblclick(function(e){
var id, rec, item = $(e.target);
if (!item.hasClass('taskhead'))
item = item.closest('div.taskhead');
if (item.length && (id = item.data('id')) && (rec = listdata[id])) {
var list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {};
if (rec.readonly || list.readonly)
task_show_dialog(id);
else
task_edit_dialog(id, 'edit');
clearSelection();
}
});
// handle global document clicks: close popup menus
$(document.body).click(clear_popups);
// extended datepicker settings
var extended_datepicker_settings = $.extend({
showButtonPanel: true,
beforeShow: function(input, inst) {
setTimeout(function(){
$(input).datepicker('widget').find('button.ui-datepicker-close')
.html(rcmail.gettext('nodate','tasklist'))
.attr('onclick', '')
.click(function(e){
$(input).datepicker('setDate', null).datepicker('hide');
});
}, 1);
},
}, datepicker_settings);
}
/**
* Request counts from the server
*/
function fetch_counts()
{
var active = active_lists();
if (active.length)
rcmail.http_request('counts', { lists:active.join(',') });
else
update_counts({});
}
/**
* List tasks matching the given selector
*/
function list_tasks(sel)
{
if (rcmail.busy)
return;
if (sel && filter_masks[sel] !== undefined) {
filtermask = filter_masks[sel];
selector = sel;
}
var active = active_lists(),
basefilter = filtermask == FILTER_MASK_COMPLETE ? FILTER_MASK_COMPLETE : FILTER_MASK_ALL,
reload = active.join(',') != loadstate.lists || basefilter != loadstate.filter || loadstate.search != search_query;
if (active.length && reload) {
ui_loading = rcmail.set_busy(true, 'loading');
rcmail.http_request('fetch', { filter:basefilter, lists:active.join(','), q:search_query }, true);
}
else if (reload)
data_ready([]);
else
render_tasklist();
$('#taskselector li.selected').removeClass('selected');
$('#taskselector li.'+selector).addClass('selected');
}
/**
* Callback if task data from server is ready
*/
function data_ready(response)
{
listdata = {};
listindex = [];
loadstate.lists = response.lists;
loadstate.filter = response.filter;
loadstate.search = response.search;
for (var i=0; i < response.data.length; i++) {
listdata[response.data[i].id] = response.data[i];
listindex.push(response.data[i].id);
}
render_tasklist();
append_tags(response.tags || []);
rcmail.set_busy(false, 'loading', ui_loading);
}
/**
*
*/
function render_tasklist()
{
// clear display
var id, rec,
count = 0,
msgbox = $('#listmessagebox').hide(),
list = $(rcmail.gui_objects.resultlist).html('');
for (var i=0; i < listindex.length; i++) {
id = listindex[i];
rec = listdata[id];
if (match_filter(rec)) {
render_task(rec);
count++;
}
}
if (!count)
msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
}
function append_tags(taglist)
{
// find new tags
var newtags = [];
for (var i=0; i < taglist.length; i++) {
if (tags.indexOf(taglist[i]) < 0)
newtags.push(taglist[i]);
}
tags = tags.concat(newtags);
// append new tags to tag cloud
$.each(newtags, function(i, tag){
$('<li>').attr('rel', tag).data('value', tag).html(Q(tag)).appendTo(rcmail.gui_objects.tagslist);
});
// re-sort tags list
$(rcmail.gui_objects.tagslist).children('li').sortElements(function(a,b){
return $.text([a]).toLowerCase() > $.text([b]).toLowerCase() ? 1 : -1;
});
}
/**
*
*/
function update_counts(counts)
{
// got new data
if (counts)
taskcounts = counts;
// iterate over all selector links and update counts
$('#taskselector a').each(function(i, elem){
var link = $(elem),
f = link.parent().attr('class').replace(/\s\w+/, '');
link.children('span').html(taskcounts[f] || '')[(taskcounts[f] ? 'show' : 'hide')]();
});
// spacial case: overdue
$('#taskselector li.overdue')[(taskcounts.overdue ? 'removeClass' : 'addClass')]('inactive');
}
/**
* Callback from server to update a single task item
*/
function update_taskitem(rec)
{
var id = rec.id,
oldid = rec.tempid || id;
oldindex = listindex.indexOf(oldid);
if (oldindex >= 0)
listindex[oldindex] = id;
else
listindex.push(id);
listdata[id] = rec;
render_task(rec, oldid);
append_tags(rec.tags || []);
}
/**
* Submit the given (changed) task record to the server
*/
function save_task(rec, action)
{
if (!rcmail.busy) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
return true;
}
return false;
}
/**
* Remove saving lock and free the UI for new input
*/
function unlock_saving()
{
if (saving_lock)
rcmail.set_busy(false, null, saving_lock);
}
/**
* Render the given task into the tasks list
*/
function render_task(rec, replace)
{
var tags_html = '';
for (var j=0; rec.tags && j < rec.tags.length; j++)
tags_html += '<span class="tag">' + Q(rec.tags[j]) + '</span>';
var div = $('<div>').addClass('taskhead').html(
'<div class="progressbar"><div class="progressvalue" style="width:' + (rec.complete * 100) + '%"></div></div>' +
'<input type="checkbox" name="completed[]" value="1" class="complete" ' + (rec.complete == 1.0 ? 'checked="checked" ' : '') + '/>' +
'<span class="flagged"></span>' +
'<span class="title">' + Q(rec.title) + '</span>' +
'<span class="tags">' + tags_html + '</span>' +
'<span class="date">' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '</span>' +
'<a href="#" class="actions">V</a>'
)
.data('id', rec.id)
.draggable({
revert: 'invalid',
addClasses: false,
cursorAt: { left:-10, top:12 },
helper: draggable_helper,
appendTo: 'body',
start: draggable_start,
stop: draggable_stop,
revertDuration: 300
});
if (rec.complete == 1.0)
div.addClass('complete');
if (rec.flagged)
div.addClass('flagged');
if (!rec.date)
div.addClass('nodate');
if ((rec.mask & FILTER_MASK_OVERDUE))
div.addClass('overdue');
var li, parent = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : null;
if (replace && (li = $('li[rel="'+replace+'"]', rcmail.gui_objects.resultlist)) && li.length) {
li.children('div.taskhead').first().replaceWith(div);
li.attr('rel', rec.id);
}
else {
li = $('<li>')
.attr('rel', rec.id)
.addClass('taskitem')
.append(div)
.append('<ul class="childtasks"></ul>');
if (!parent || !parent.length)
li.appendTo(rcmail.gui_objects.resultlist);
}
if (parent && parent.length)
li.appendTo(parent);
if (replace) {
resort_task(rec, li, true);
// TODO: remove the item after a while if it doesn't match the current filter anymore
}
}
/**
* Move the given task item to the right place in the list
*/
function resort_task(rec, li, animated)
{
var dir = 0, index, slice, next_li, next_id, next_rec;
// animated moving
var insert_animated = function(li, before, after) {
if (before && li.next().get(0) == before.get(0))
return; // nothing to do
else if (after && li.prev().get(0) == after.get(0))
return; // nothing to do
var speed = 300;
li.slideUp(speed, function(){
if (before) li.insertBefore(before);
else if (after) li.insertAfter(after);
li.slideDown(speed);
});
}
// remove from list index
+ var oldlist = listindex.join('%%%');
var oldindex = listindex.indexOf(rec.id);
if (oldindex >= 0) {
slice = listindex.slice(0,oldindex);
listindex = slice.concat(listindex.slice(oldindex+1));
}
// find the right place to insert the task item
li.siblings().each(function(i, elem){
next_li = $(elem);
next_id = next_li.attr('rel');
next_rec = listdata[next_id];
if (next_id == rec.id) {
next_li = null;
return 1; // continue
}
if (next_rec && task_cmp(rec, next_rec) > 0) {
return 1; // continue;
}
else if (next_rec && next_li && task_cmp(rec, next_rec) < 0) {
if (animated) insert_animated(li, next_li);
else li.insertBefore(next_li);
next_li = null;
return false;
}
});
index = listindex.indexOf(next_id);
if (next_li) {
if (animated) insert_animated(li, null, next_li);
else li.insertAfter(next_li);
index++;
}
// insert into list index
if (next_id && index >= 0) {
slice = listindex.slice(0,index);
slice.push(rec.id);
listindex = slice.concat(listindex.slice(index));
}
+ else { // restore old list index
+ listindex = oldlist.split('%%%');
+ }
}
/**
* Compare function of two task records.
* (used for sorting)
*/
function task_cmp(a, b)
{
var d = Math.floor(a.complete) - Math.floor(b.complete);
if (!d) d = (b._hasdate-0) - (a._hasdate-0);
if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999);
return d;
}
/* Helper functions for drag & drop functionality */
function draggable_helper()
{
if (!draghelper)
draghelper = $('<div class="taskitem-draghelper">✔</div>');
return draghelper;
}
function draggable_start(event, ui)
{
$('.taskhead, #rootdroppable').droppable({
hoverClass: 'droptarget',
accept: droppable_accept,
drop: draggable_dropped,
addClasses: false
});
$(this).parent().addClass('dragging');
$('#rootdroppable').show();
}
function draggable_stop(event, ui)
{
$(this).parent().removeClass('dragging');
$('#rootdroppable').hide();
}
function droppable_accept(draggable)
{
var drag_id = draggable.data('id'),
parent_id = $(this).data('id'),
drag_rec = listdata[drag_id],
drop_rec = listdata[parent_id];
if (drop_rec && drop_rec.list != drag_rec.list)
return false;
if (parent_id == drag_rec.parent_id)
return false;
while (drop_rec && drop_rec.parent_id) {
if (drop_rec.parent_id == drag_id)
return false;
drop_rec = listdata[drop_rec.parent_id];
}
return true;
}
function draggable_dropped(event, ui)
{
var parent_id = $(this).data('id'),
task_id = ui.draggable.data('id'),
parent = parent_id ? $('li[rel="'+parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : $(rcmail.gui_objects.resultlist),
rec = listdata[task_id],
li;
if (rec && parent.length) {
// submit changes to server
rec.parent_id = parent_id || 0;
save_task(rec, 'edit');
li = ui.draggable.parent();
li.slideUp(300, function(){
li.appendTo(parent);
resort_task(rec, li);
li.slideDown(300);
});
}
}
/**
* Show task details in a dialog
*/
function task_show_dialog(id)
{
var $dialog = $('#taskshow').dialog('close'), rec;;
if (!(rec = listdata[id]) || clear_popups({}))
return;
me.selected_task = rec;
// fill dialog data
$('#task-parent-title').html(Q(rec.parent_title || '')+' »').css('display', rec.parent_title ? 'block' : 'none');
$('#task-title').html(Q(rec.title || ''));
$('#task-description').html(text2html(rec.description || '', 300, 6))[(rec.description ? 'show' : 'hide')]();
$('#task-date')[(rec.date ? 'show' : 'hide')]().children('.task-text').html(Q(rec.date || rcmail.gettext('nodate','tasklist')));
$('#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-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
$('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
var taglist = $('#task-tags')[(rec.tags && rec.tags.length ? 'show' : 'hide')]().children('.task-text').empty();
if (rec.tags && rec.tags.length) {
$.each(rec.tags, function(i,val){
$('<span>').addClass('tag-element').html(Q(val)).data('value', val).appendTo(taglist);
});
}
+ // 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() {
task_edit_dialog(me.selected_task.id, 'edit');
$dialog.dialog('close');
};
buttons[rcmail.gettext('delete','tasklist')] = function() {
if (delete_task(me.selected_task.id))
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: true,
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,
width: 580
}).show();
}
/**
* Opens the dialog to edit a task
*/
function task_edit_dialog(id, action, presets)
{
$('#taskshow').dialog('close');
var rec = listdata[id] || presets,
$dialog = $('<div>'),
editform = $('#taskedit'),
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 || '');
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);
completeness_slider.slider('value', complete.val());
var tasklist = $('#edit-tasklist').val(rec.list || 0); // .prop('disabled', rec.parent_id ? true : false);
// tag-edit line
var tagline = $(rcmail.gui_objects.edittagline).empty();
$.each(typeof rec.tags == 'object' && rec.tags.length ? rec.tags : [''], function(i,val){
$('<input>')
.attr('name', 'tags[]')
.attr('tabindex', '3')
.addClass('tag')
.val(val)
.appendTo(tagline);
});
$('input.tag', rcmail.gui_objects.edittagline).tagedit({
animSpeed: 100,
allowEdit: false,
checkNewEntriesCaseSensitive: false,
autocompleteOptions: { source: tags, minLength: 0 },
texts: { removeLinkTitle: rcmail.gettext('removetag', 'tasklist') }
});
$('a.edit-nodate').unbind('click').click(function(){
var sel = $(this).attr('rel');
if (sel) $(sel).val('');
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() {
// copy form field contents into task object to save
$.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, list:tasklist }, function(key,input){
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) {
var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.startdate, datepicker_settings);
var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.date, datepicker_settings);
if (startdate > duedate) {
alert(rcmail.gettext('invalidstartduedates', 'tasklist'));
return false;
}
}
$('input[name="tags[]"]', rcmail.gui_objects.edittagline).each(function(i,elem){
if (elem.value)
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))
me.selected_task.complete = null;
if (!me.selected_task.list && list.id)
me.selected_task.list = list.id;
if (save_task(me.selected_task, action))
$dialog.dialog('close');
};
if (rec.id) {
buttons[rcmail.gettext('delete', 'tasklist')] = function() {
if (delete_task(rec.id))
$dialog.dialog('close');
};
}
buttons[rcmail.gettext('cancel', 'tasklist')] = function() {
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
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,
minWidth: 500,
width: 580
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE
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];
+ };
+
/**
*
*/
function add_childtask(id)
{
task_edit_dialog(null, 'new', { parent_id:id });
}
/**
* Delete the given task
*/
function delete_task(id)
{
var rec = listdata[id];
if (rec && confirm("Delete this?")) {
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('task', { action:'delete', t:rec, filter:filtermask });
$('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
return true;
}
return false;
}
/**
* Check if the given task matches the current filtermask and tag selection
*/
function match_filter(rec)
{
var match = !filtermask || (filtermask & rec.mask) > 0;
if (match && tagsfilter.length) {
match = rec.tags && rec.tags.length;
for (var i=0; match && i < tagsfilter.length; i++) {
if (rec.tags.indexOf(tagsfilter[i]) < 0)
match = false;
}
}
return match;
}
/**
*
*/
function list_edit_dialog(id)
{
var list = me.tasklists[id],
$dialog = $('#tasklistform').dialog('close');
editform = $('#tasklisteditform');
if (!list)
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);
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('save','tasklist')] = function() {
// do some input validation
if (!name.val() || name.val().length < 2) {
alert(rcmail.gettext('invalidlistproperties', 'tasklist'));
name.select();
return;
}
// post data to server
var data = editform.serializeJSON();
if (list.id)
data.id = list.id;
if (alarms)
data.showalarms = alarms.checked ? 1 : 0;
if (parent.length)
data.parentfolder = $('option:selected', parent).val();
saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
rcmail.http_post('tasklist', { action:(list.id ? 'edit' : 'new'), l:data });
$dialog.dialog('close');
};
buttons[rcmail.gettext('cancel','tasklist')] = function() {
$dialog.dialog('close');
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext((list.id ? 'editlist' : 'createlist'), 'tasklist'),
close: function() { $dialog.dialog('destroy').hide(); },
buttons: buttons,
minWidth: 400,
width: 420
}).show();
}
/**
*
*/
function list_remove(id)
{
var list = me.tasklists[id];
if (list && !list.readonly) {
alert('To be implemented')
}
}
/**
*
*/
function insert_list(prop)
{
var li = $('<li>').attr('id', 'rcmlitasklist'+prop.id)
.append('<input type="checkbox" name="_list[]" value="'+prop.id+'" checked="checked" />')
.append('<span class="handle"> </span>')
.append('<span class="listname">'+Q(prop.name)+'</span>');
$(rcmail.gui_objects.folderlist).append(li);
init_tasklist_li(li.get(0), prop.id);
me.tasklists[prop.id] = prop;
}
/**
*
*/
function update_list(prop)
{
var id = prop.oldid || prop.id,
li = rcmail.get_folder_li(id, 'rcmlitasklist');
if (me.tasklists[id] && li) {
delete me.tasklists[id];
me.tasklists[prop.id] = prop;
$(li).data('id', prop.id);
$('#'+li.id+' input').data('id', prop.id);
$('.listname', li).html(Q(prop.name));
}
}
/**
* Execute search
*/
function quicksearch()
{
var q;
if (rcmail.gui_objects.qsearchbox && (q = rcmail.gui_objects.qsearchbox.value)) {
var id = 'search-'+q;
var resources = [];
for (var rid in me.tasklists) {
if (me.tasklists[rid].active) {
resources.push(rid);
}
}
id += '@'+resources.join(',');
// ignore if query didn't change
if (search_request == id)
return;
search_request = id;
search_query = q;
list_tasks('all');
}
else // empty search input equals reset
this.reset_search();
}
/**
* Reset search and get back to normal listing
*/
function reset_search()
{
$(rcmail.gui_objects.qsearchbox).val('');
if (search_request) {
search_request = search_query = null;
list_tasks();
}
}
/**** Utility functions ****/
/**
* quote html entities
*/
function Q(str)
{
return String(str).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
}
/**
* Name says it all
* (cloned from calendar plugin)
*/
function text2html(str, maxlen, maxlines)
{
var html = Q(String(str));
// limit visible text length
if (maxlen) {
var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','tasklist')+'</a><span style="display:none">',
lines = html.split(/\r?\n/),
words, out = '', len = 0;
for (var i=0; i < lines.length; i++) {
len += lines[i].length;
if (maxlines && i == maxlines - 1) {
out += lines[i] + '\n' + morelink;
maxlen = html.length * 2;
}
else if (len > maxlen) {
len = out.length;
words = lines[i].split(' ');
for (var j=0; j < words.length; j++) {
len += words[j].length + 1;
out += words[j] + ' ';
if (len > maxlen) {
out += morelink;
maxlen = html.length * 2;
}
}
out += '\n';
}
else
out += lines[i] + '\n';
}
if (maxlen > str.length)
out += '</span>';
html = out;
}
// simple link parser (similar to rcube_string_replacer class in PHP)
var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
return html
.replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
.replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
.replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
.replace(/\n/g, "<br/>");
}
/**
* Clear any text selection
* (text is probably selected when double-clicking somewhere)
*/
function clearSelection()
{
if (document.selection && document.selection.empty) {
document.selection.empty() ;
}
else if (window.getSelection) {
var sel = window.getSelection();
if (sel && sel.removeAllRanges)
sel.removeAllRanges();
}
}
/**
* Hide all open popup menus
*/
function clear_popups(e)
{
var count = 0, target = e.target;
if (target && target.className == 'inner')
target = e.target.parentNode;
$('.popupmenu:visible').each(function(i, elem){
var menu = $(elem), id = elem.id;
if (target.id != id+'link' && (!menu.data('sticky') || !target_overlaps(e.target, elem))) {
menu.hide();
count++;
}
});
return count;
}
/**
* Check whether the event target is a descentand of the given element
*/
function target_overlaps(target, elem)
{
while (target.parentNode) {
if (target.parentNode == elem)
return true;
target = target.parentNode;
}
return false;
}
/**
*
*/
function active_lists()
{
var active = [];
for (var id in me.tasklists) {
if (me.tasklists[id].active)
active.push(id);
}
return active;
}
/**
* Register event handlers on a tasklist (folder) item
*/
function init_tasklist_li(li, id)
{
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.tasklists[id]) { // add or remove event source on click
me.tasklists[id].active = this.checked;
fetch_counts();
list_tasks(null);
rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:me.tasklists[id].active?1:0 } });
}
}).data('id', id).get(0).checked = me.tasklists[id].active || false;
$(li).click(function(e){
var id = $(this).data('id');
rcmail.select_folder(id, 'rcmlitasklist');
rcmail.enable_command('list-edit', 'list-remove', 'import', !me.tasklists[id].readonly);
me.selected_list = id;
})
.data('id', id);
}
}
// extend jQuery
(function($){
$.fn.serializeJSON = function(){
var json = {};
jQuery.map($(this).serializeArray(), function(n, i) {
json[n['name']] = n['value'];
});
return json;
};
})(jQuery);
// from http://james.padolsey.com/javascript/sorting-elements-with-jquery/
jQuery.fn.sortElements = (function(){
var sort = [].sort;
return function(comparator, getSortable) {
getSortable = getSortable || function(){ return this };
var last = null;
return sort.call(this, comparator).each(function(i){
// at this point the array is sorted, so we can just detach each one from wherever it is, and add it after the last
var node = $(getSortable.call(this));
var parent = node.parent();
if (last) last.after(node);
else parent.prepend(node);
last = node;
});
};
})();
/* tasklist plugin UI initialization */
var rctasks;
window.rcmail && rcmail.addEventListener('init', function(evt) {
rctasks = new rcube_tasklist_ui(rcmail.env.tasklist_settings);
// register button commands
rcmail.register_command('newtask', function(){ rctasks.edit_task(null, 'new', {}); }, true);
//rcmail.register_command('print', function(){ rctasks.print_list(); }, true);
rcmail.register_command('list-create', function(){ rctasks.list_edit_dialog(null); }, true);
rcmail.register_command('list-edit', function(){ rctasks.list_edit_dialog(rctasks.selected_list); }, false);
rcmail.register_command('list-remove', function(){ rctasks.list_remove(rctasks.selected_list); }, false);
rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
rctasks.init();
});
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 4247e14c..6dec0d40 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1,645 +1,892 @@
<?php
/**
* Tasks plugin for Roundcube webmail
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class tasklist extends rcube_plugin
{
const FILTER_MASK_TODAY = 1;
const FILTER_MASK_TOMORROW = 2;
const FILTER_MASK_WEEK = 4;
const FILTER_MASK_LATER = 8;
const FILTER_MASK_NODATE = 16;
const FILTER_MASK_OVERDUE = 32;
const FILTER_MASK_FLAGGED = 64;
const FILTER_MASK_COMPLETE = 128;
public static $filter_masks = array(
'today' => self::FILTER_MASK_TODAY,
'tomorrow' => self::FILTER_MASK_TOMORROW,
'week' => self::FILTER_MASK_WEEK,
'later' => self::FILTER_MASK_LATER,
'nodate' => self::FILTER_MASK_NODATE,
'overdue' => self::FILTER_MASK_OVERDUE,
'flagged' => self::FILTER_MASK_FLAGGED,
'complete' => self::FILTER_MASK_COMPLETE,
);
public $task = '?(?!login|logout).*';
public $rc;
public $driver;
public $timezone;
public $ui;
public $defaults = array(
'date_format' => "Y-m-d",
'time_format' => "H:i",
'first_day' => 1,
);
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcmail::get_instance();
$this->register_task('tasks', 'tasklist');
// load plugin configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $this->rc->task == 'tasks' && (!$this->rc->action || $this->rc->action == 'print'));
if ($this->rc->task == 'tasks' && $this->rc->action != 'save-pref') {
$this->load_driver();
// register calendar actions
$this->register_action('index', array($this, 'tasklist_view'));
$this->register_action('task', array($this, 'task_action'));
$this->register_action('tasklist', array($this, 'tasklist_action'));
$this->register_action('counts', array($this, 'fetch_counts'));
$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
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
// $this->add_hook('message_load', array($this, 'mail_message_load'));
// $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'tasklist-create-from-mail',
'label' => 'tasklist.createfrommail',
'type' => 'link',
'classact' => 'icon taskaddlink active',
'class' => 'icon taskaddlink',
'innerclass' => 'icon taskadd',
))),
'messagemenu');
}
}
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
require_once($this->home . '/tasklist_ui.php');
$this->ui = new tasklist_ui($this);
$this->ui->init();
}
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver))
return;
$driver_name = $this->rc->config->get('tasklist_driver', 'database');
$driver_class = 'tasklist_' . $driver_name . '_driver';
require_once($this->home . '/drivers/tasklist_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
$this->require_plugin('libkolab');
default:
$this->driver = new $driver_class($this);
break;
}
// get user's timezone
$this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
}
/**
*
*/
public function task_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$rec = get_input_value('t', RCUBE_INPUT_POST, true);
$oldrec = $rec;
$success = $refresh = false;
switch ($action) {
case 'new':
$oldrec = null;
$rec = $this->prepare_task($rec);
$rec['uid'] = $this->generate_uid();
$temp_id = $rec['tempid'];
if ($success = $this->driver->create_task($rec)) {
$refresh = $this->driver->get_task($rec);
if ($temp_id) $refresh['tempid'] = $temp_id;
}
break;
case 'edit':
$rec = $this->prepare_task($rec);
if ($success = $this->driver->edit_task($rec))
$refresh = $this->driver->get_task($rec);
break;
case 'delete':
if (!($success = $this->driver->delete_task($rec, false)))
$this->rc->output->command('plugin.reload_data');
break;
case 'undelete':
if ($success = $this->driver->undelete_task($rec))
$refresh = $this->driver->get_task($rec);
break;
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
$this->update_counts($oldrec, $refresh);
}
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
// unlock client
$this->rc->output->command('plugin.unlock_saving');
if ($refresh) {
$this->encode_task($refresh);
$this->rc->output->command('plugin.refresh_task', $refresh);
}
}
/**
* repares new/edited task properties before save
*/
private function prepare_task($rec)
{
// try to be smart and extract date from raw input
if ($rec['raw']) {
foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
$locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
$normwords[] = $word;
$datewords[] = $word;
}
foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
$locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
$normwords[] = $month;
$datewords[] = $month;
}
foreach (array('on','this','next','at') as $word) {
$fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
$fillwords[] = $word;
}
$raw = trim($rec['raw']);
$date_str = '';
// translate localized keywords
$raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
$raw = preg_replace($locwords, $normwords, $raw);
// find date pattern
$date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
if (preg_match($date_pattern, $raw, $m)) {
$date_str .= $m[1] . $m[2] . $m[3];
$raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
// add year to date string
if ($m[1] && !$m[3])
$date_str .= date('Y');
}
// find time pattern
$time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
if (preg_match($time_pattern, $raw, $m)) {
$has_time = true;
$date_str .= ($date_str ? ' ' : 'today ') . $m[1];
$raw = preg_replace($time_pattern, '', $raw);
}
// yes, raw input matched a (valid) date
if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
$rec['date'] = $date->format('Y-m-d');
if ($has_time)
$rec['time'] = $date->format('H:i');
$rec['title'] = $raw;
}
else
$rec['title'] = $rec['raw'];
}
// normalize input from client
if (isset($rec['complete'])) {
$rec['complete'] = floatval($rec['complete']);
if ($rec['complete'] > 1)
$rec['complete'] /= 100;
}
if (isset($rec['flagged']))
$rec['flagged'] = intval($rec['flagged']);
// fix for garbage input
if ($rec['description'] == 'null')
$rec['description'] = '';
foreach ($rec as $key => $val) {
if ($val === 'null')
$rec[$key] = null;
}
if (!empty($rec['date'])) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['date'] = $date->format('Y-m-d');
if (!empty($rec['time']))
$rec['time'] = $date->format('H:i');
}
catch (Exception $e) {
$rec['date'] = $rec['time'] = null;
}
}
if (!empty($rec['startdate'])) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdate'] = $date->format('Y-m-d');
if (!empty($rec['starttime']))
$rec['starttime'] = $date->format('H:i');
}
catch (Exception $e) {
$rec['startdate'] = $rec['starttime'] = null;
}
}
+ $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;
}
/**
*
*/
public function tasklist_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$list = get_input_value('l', RCUBE_INPUT_POST, true);
$success = false;
if (isset($list['showalarms']))
$list['showalarms'] = intval($list['showalarms']);
switch ($action) {
case 'new':
$list += array('showalarms' => true, 'active' => true);
if ($insert_id = $this->driver->create_list($list)) {
$list['id'] = $insert_id;
$this->rc->output->command('plugin.insert_tasklist', $list);
$success = true;
}
break;
case 'edit':
if ($newid = $this->driver->edit_list($list)) {
$list['oldid'] = $list['id'];
$list['id'] = $newid;
$this->rc->output->command('plugin.update_tasklist', $list);
$success = true;
}
break;
case 'subscribe':
$success = $this->driver->subscribe_list($list);
break;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('tasklist.errorsaving', 'error');
$this->rc->output->command('plugin.unlock_saving');
}
/**
*
*/
public function fetch_counts()
{
$lists = get_input_value('lists', RCUBE_INPUT_GPC);;
$counts = $this->driver->count_tasks($lists);
$this->rc->output->command('plugin.update_counts', $counts);
}
/**
* Adjust the cached counts after changing a task
*
*
*/
public function update_counts($oldrec, $newrec)
{
// rebuild counts until this function is finally implemented
$this->fetch_counts();
// $this->rc->output->command('plugin.update_counts', $counts);
}
/**
*
*/
public function fetch_tasks()
{
$f = intval(get_input_value('filter', RCUBE_INPUT_GPC));
$search = get_input_value('q', RCUBE_INPUT_GPC);
$filter = array('mask' => $f, 'search' => $search);
$lists = get_input_value('lists', RCUBE_INPUT_GPC);;
/*
// convert magic date filters into a real date range
switch ($f) {
case self::FILTER_MASK_TODAY:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $filter['to'] = $today->format('Y-m-d');
break;
case self::FILTER_MASK_TOMORROW:
$tomorrow = new DateTime('now + 1 day', $this->timezone);
$filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d');
break;
case self::FILTER_MASK_OVERDUE:
$yesterday = new DateTime('yesterday', $this->timezone);
$filter['to'] = $yesterday->format('Y-m-d');
break;
case self::FILTER_MASK_WEEK:
$today = new DateTime('now', $this->timezone);
$filter['from'] = $today->format('Y-m-d');
$weekend = new DateTime('now + 7 days', $this->timezone);
$filter['to'] = $weekend->format('Y-m-d');
break;
case self::FILTER_MASK_LATER:
$date = new DateTime('now + 8 days', $this->timezone);
$filter['from'] = $date->format('Y-m-d');
break;
}
*/
$data = $tags = $this->task_tree = $this->task_titles = array();
foreach ($this->driver->list_tasks($filter, $lists) as $rec) {
if ($rec['parent_id']) {
$this->task_tree[$rec['id']] = $rec['parent_id'];
}
$this->encode_task($rec);
if (!empty($rec['tags']))
$tags = array_merge($tags, (array)$rec['tags']);
// apply filter; don't trust the driver on this :-)
if ((!$f && $rec['complete'] < 1.0) || ($rec['mask'] & $f))
$data[] = $rec;
}
// sort tasks according to their hierarchy level and due date
usort($data, array($this, 'task_sort_cmp'));
$this->rc->output->command('plugin.data_ready', array('filter' => $f, 'lists' => $lists, 'search' => $search, 'data' => $data, 'tags' => array_values(array_unique($tags))));
}
/**
* Prepare the given task record before sending it to the client
*/
private function encode_task(&$rec)
{
$rec['mask'] = $this->filter_mask($rec);
$rec['flagged'] = intval($rec['flagged']);
$rec['complete'] = floatval($rec['complete']);
$rec['changed'] = is_object($rec['changed']) ? $rec['changed']->format('U') : null;
if ($rec['date']) {
try {
$date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
$rec['datetime'] = intval($date->format('U'));
$rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
$rec['_hasdate'] = 1;
}
catch (Exception $e) {
$rec['date'] = $rec['datetime'] = null;
}
}
else {
$rec['date'] = $rec['datetime'] = null;
$rec['_hasdate'] = 0;
}
if ($rec['startdate']) {
try {
$date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
$rec['startdatetime'] = intval($date->format('U'));
$rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
}
catch (Exception $e) {
$rec['startdate'] = $rec['startdatetime'] = null;
}
}
+ 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']];
while ($parent_id) {
$rec['_depth']++;
$rec['parent_title'] = $this->task_titles[$parent_id];
$parent_id = $this->task_tree[$parent_id];
}
}
$this->task_titles[$rec['id']] = $rec['title'];
}
/**
* Compare function for task list sorting.
* Nested tasks need to be sorted to the end.
*/
private function task_sort_cmp($a, $b)
{
$d = $a['_depth'] - $b['_depth'];
if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
if (!$d) $d = $a['datetime'] - $b['datetime'];
return $d;
}
/**
* Compute the filter mask of the given task
*
* @param array Hash array with Task record properties
* @return int Filter mask
*/
public function filter_mask($rec)
{
static $today, $tomorrow, $weeklimit;
if (!$today) {
$today_date = new DateTime('now', $this->timezone);
$today = $today_date->format('Y-m-d');
$tomorrow_date = new DateTime('now + 1 day', $this->timezone);
$tomorrow = $tomorrow_date->format('Y-m-d');
$week_date = new DateTime('now + 7 days', $this->timezone);
$weeklimit = $week_date->format('Y-m-d');
}
$mask = 0;
$start = $rec['startdate'] ?: '1900-00-00';
if ($rec['flagged'])
$mask |= self::FILTER_MASK_FLAGGED;
if ($rec['complete'] == 1.0)
$mask |= self::FILTER_MASK_COMPLETE;
if (empty($rec['date']))
$mask |= self::FILTER_MASK_NODATE;
else if ($rec['date'] < $today)
$mask |= self::FILTER_MASK_OVERDUE;
if ($rec['date'] >= $today && $start <= $today)
$mask |= self::FILTER_MASK_TODAY;
if ($rec['date'] >= $tomorrow && $start <= $tomorrow)
$mask |= self::FILTER_MASK_TOMORROW;
if (($start > $tomorrow || $rec['date'] > $tomorrow) && $rec['date'] <= $weeklimit)
$mask |= self::FILTER_MASK_WEEK;
if ($start > $weeklimit || $rec['date'] > $weeklimit)
$mask |= self::FILTER_MASK_LATER;
return $mask;
}
/******* UI functions ********/
/**
* Render main view of the tasklist task
*/
public function tasklist_view()
{
$this->ui->init();
$this->ui->init_templates();
$this->rc->output->set_pagetitle($this->gettext('navtitle'));
$this->rc->output->send('tasklist.mainview');
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['tasklist.'.$label] = $this->gettext($label);
$texts['tasklist.newtask'] = $this->gettext('createfrommail');
$this->ui->init_templates();
echo $this->api->output->parse('tasklist.taskedit', false, false);
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
// "rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
// "rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
// "rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
}
+ /******* 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()
{
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$task = array();
// establish imap connection
$imap = $this->rc->get_storage();
$imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$task['title'] = trim($message->subject);
$task['description'] = trim($message->first_text_part());
/*
// copy mail attachments to event
if ($message->attachments) {
$eventid = 'cal:';
if (!is_array($_SESSION['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
$_SESSION['event_session'] = array();
$_SESSION['event_session']['id'] = $eventid;
$_SESSION['event_session']['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $eventid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION['event_session']['attachments'][$id] = $attachment;
$attachment['id'] = 'rcmfile' . $attachment['id']; # add prefix to consider it 'new'
$event['attachments'][] = $attachment;
}
}
}
*/
$this->rc->output->command('plugin.mail2taskdialog', $task);
}
else {
$this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
}
$this->rc->output->send();
}
/******* Utility functions *******/
/**
* Generate a unique identifier for an event
*/
public function generate_uid()
{
return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
}
}
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 43f79c9f..370237e5 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -1,237 +1,321 @@
<?php
/**
* User Interface class for the Tasklist plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class tasklist_ui
{
private $rc;
private $plugin;
private $ready = false;
function __construct($plugin)
{
$this->plugin = $plugin;
$this->rc = $plugin->rc;
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
if ($this->ready) // already done
return;
// add taskbar button
$this->plugin->add_button(array(
'command' => 'tasks',
'class' => 'button-tasklist',
'classsel' => 'button-tasklist button-selected',
'innerclass' => 'button-inner',
'label' => 'tasklist.navtitle',
), 'taskbar');
$this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tasklist.css');
$this->plugin->include_script('tasklist_base.js');
// copy config to client
$defaults = $this->plugin->defaults;
$settings = array(
'date_format' => $this->rc->config->get('date_format', $defaults['date_format']),
'time_format' => $this->rc->config->get('time_format', $defaults['time_format']),
'first_day' => $this->rc->config->get('calendar_first_day', $defaults['first_day']),
);
$this->rc->output->set_env('tasklist_settings', $settings);
$this->ready = true;
}
/**
* Register handler methods for the template engine
*/
public function init_templates()
{
$this->plugin->register_handler('plugin.tasklists', array($this, 'tasklists'));
$this->plugin->register_handler('plugin.tasklist_select', array($this, 'tasklist_select'));
$this->plugin->register_handler('plugin.category_select', array($this, 'category_select'));
$this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
$this->plugin->register_handler('plugin.quickaddform', array($this, 'quickadd_form'));
$this->plugin->register_handler('plugin.tasklist_editform', array($this, 'tasklist_editform'));
$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');
}
/**
*
*/
function tasklists($attrib = array())
{
$lists = $this->plugin->driver->get_lists();
$li = '';
foreach ((array)$lists as $id => $prop) {
if ($attrib['activeonly'] && !$prop['active'])
continue;
unset($prop['user_id']);
$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);
$class = 'tasks-' . asciiwords($id, true);
if ($prop['readonly'])
$class .= ' readonly';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
$li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
html::span('handle', ' ') .
html::span('listname', Q($prop['name'])));
}
$this->rc->output->set_env('tasklists', $jsenv);
$this->rc->output->add_gui_object('folderlist', $attrib['id']);
return html::tag('ul', $attrib, $li, html::$common_attrib);
}
/**
* Render a HTML select box for list selection
*/
function tasklist_select($attrib = array())
{
$attrib['name'] = 'list';
$select = new html_select($attrib);
foreach ((array)$this->plugin->driver->get_lists() as $id => $prop) {
if (!$prop['readonly'])
$select->add($prop['name'], $id);
}
return $select->show(null);
}
function tasklist_editform($attrib = array())
{
$fields = array(
'name' => array(
'id' => 'edit-tasklistame',
'label' => $this->plugin->gettext('listname'),
'value' => html::tag('input', array('id' => 'edit-tasklistame', 'name' => 'name', 'type' => 'text', 'class' => 'text', 'size' => 40)),
),
/*
'color' => array(
'id' => 'edit-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')),
),
*/
);
return html::tag('form', array('action' => "#", 'method' => "post", 'id' => 'tasklisteditform'),
$this->plugin->driver->tasklist_edit_form($fields)
);
}
/**
* Render a HTML select box to select a task category
*/
function category_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);
}
return $select->show(null);
}
/**
*
*/
function quickadd_form($attrib)
{
$attrib += array('action' => $this->rc->url('add'), 'method' => 'post', 'id' => 'quickaddform');
$input = new html_inputfield(array('name' => 'text', 'id' => 'quickaddinput', 'placeholder' => $this->plugin->gettext('createnewtask')));
$button = html::tag('input', array('type' => 'submit', 'value' => '+', 'class' => 'button mainaction'));
$this->rc->output->add_gui_object('quickaddform', $attrib['id']);
return html::tag('form', $attrib, $input->show() . $button);
}
/**
* The result view
*/
function tasks_resultview($attrib)
{
$attrib += array('id' => 'rcmtaskslist');
$this->rc->output->add_gui_object('resultlist', $attrib['id']);
unset($attrib['name']);
return html::tag('ul', $attrib, '');
}
/**
* Container for a tags cloud
*/
function tagslist($attrib)
{
$attrib += array('id' => 'rcmtagslist');
unset($attrib['name']);
$this->rc->output->add_gui_object('tagslist', $attrib['id']);
return html::tag('ul', $attrib, '');
}
/**
* Interactive UI element to add/remove tags
*/
function tags_editline($attrib)
{
$attrib += array('id' => 'rcmtagsedit');
$this->rc->output->add_gui_object('edittagline', $attrib['id']);
$input = new html_inputfield(array('name' => 'tags[]', 'class' => 'tag', 'size' => $attrib['size'], 'tabindex' => $attrib['tabindex']));
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);
+ }
+
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, May 1, 2:14 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
661389
Default Alt Text
(152 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment