Import of a basic task management module for Roundcube working with libkolab storage
This commit is contained in:
parent
529d16eac3
commit
aed27f7d11
15 changed files with 3412 additions and 0 deletions
1
plugins/tasklist/.gitignore
vendored
Normal file
1
plugins/tasklist/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
config.inc.php
|
4
plugins/tasklist/config.inc.php.dist
Normal file
4
plugins/tasklist/config.inc.php.dist
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
$rcmail_config['tasklist_driver'] = 'kolab';
|
||||
|
46
plugins/tasklist/drivers/database/SQL/mysql.sql
Normal file
46
plugins/tasklist/drivers/database/SQL/mysql.sql
Normal file
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Roundcube Tasklist plugin database
|
||||
*
|
||||
* @version @package_version@
|
||||
* @author Thomas Bruederli
|
||||
* @licence GNU AGPL
|
||||
* @copyright (C) 2012, Kolab Systems AG
|
||||
*/
|
||||
|
||||
CREATE TABLE `tasklists` (
|
||||
`tasklist_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(10) unsigned NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`color` varchar(8) NOT NULL,
|
||||
`showalarms` tinyint(2) unsigned NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`tasklist_id`),
|
||||
KEY `user_id` (`user_id`),
|
||||
CONSTRAINT `fk_tasklist_user_id` FOREIGN KEY (`user_id`)
|
||||
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) /*!40000 ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci */;
|
||||
|
||||
CREATE TABLE `tasks` (
|
||||
`task_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`tasklist_id` int(10) unsigned NOT NULL,
|
||||
`parent_id` int(10) unsigned DEFAULT NULL,
|
||||
`uid` varchar(255) NOT NULL,
|
||||
`created` datetime NOT NULL,
|
||||
`changed` datetime NOT NULL,
|
||||
`del` tinyint(1) unsigned NOT NULL DEFAULT '0',
|
||||
`title` varchar(255) NOT NULL,
|
||||
`description` text,
|
||||
`date` varchar(10) DEFAULT NULL,
|
||||
`time` varchar(5) DEFAULT NULL,
|
||||
`flagged` tinyint(4) NOT NULL DEFAULT '0',
|
||||
`complete` float NOT NULL DEFAULT '0',
|
||||
`alarms` varchar(255) NOT NULL,
|
||||
`recurrence` varchar(255) DEFAULT NULL,
|
||||
`organizer` varchar(255) DEFAULT NULL,
|
||||
`attendees` text,
|
||||
`notify` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`task_id`),
|
||||
KEY `tasklisting` (`tasklist_id`,`del`,`date`),
|
||||
KEY `uid` (`uid`),
|
||||
CONSTRAINT `fk_tasks_tasklist_id` FOREIGN KEY (`tasklist_id`)
|
||||
REFERENCES `tasklists`(`tasklist_id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) /*!40000 ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci */;
|
486
plugins/tasklist/drivers/database/tasklist_database_driver.php
Normal file
486
plugins/tasklist/drivers/database/tasklist_database_driver.php
Normal file
|
@ -0,0 +1,486 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Database 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_database_driver extends tasklist_driver
|
||||
{
|
||||
public $undelete = true; // yes, we can
|
||||
public $sortable = false;
|
||||
|
||||
private $rc;
|
||||
private $plugin;
|
||||
private $cache = array();
|
||||
private $lists = array();
|
||||
private $list_ids = '';
|
||||
|
||||
private $db_tasks = 'tasks';
|
||||
private $db_lists = 'tasklists';
|
||||
private $sequence_tasks = 'task_ids';
|
||||
private $sequence_lists = 'tasklist_ids';
|
||||
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct($plugin)
|
||||
{
|
||||
$this->rc = $plugin->rc;
|
||||
$this->plugin = $plugin;
|
||||
|
||||
// read database config
|
||||
$this->db_lists = $this->rc->config->get('db_table_lists', $this->db_lists);
|
||||
$this->db_tasks = $this->rc->config->get('db_table_tasks', $this->db_tasks);
|
||||
$this->sequence_lists = $this->rc->config->get('db_sequence_lists', $this->sequence_lists);
|
||||
$this->sequence_tasks = $this->rc->config->get('db_sequence_tasks', $this->sequence_tasks);
|
||||
|
||||
$this->_read_lists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read available calendars for the current user and store them internally
|
||||
*/
|
||||
private function _read_lists()
|
||||
{
|
||||
$hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', '')));
|
||||
|
||||
if (!empty($this->rc->user->ID)) {
|
||||
$list_ids = array();
|
||||
$result = $this->rc->db->query(
|
||||
"SELECT *, tasklist_id AS id FROM " . $this->db_lists . "
|
||||
WHERE user_id=?
|
||||
ORDER BY name",
|
||||
$this->rc->user->ID
|
||||
);
|
||||
|
||||
while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
|
||||
$arr['showalarms'] = intval($arr['showalarms']);
|
||||
$arr['active'] = !in_array($arr['id'], $hidden);
|
||||
$this->lists[$arr['id']] = $arr;
|
||||
$list_ids[] = $this->rc->db->quote($arr['id']);
|
||||
}
|
||||
$this->list_ids = join(',', $list_ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of available tasks 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
|
||||
* @return mixed ID of the new list on success, False on error
|
||||
* @see tasklist_driver::create_list()
|
||||
*/
|
||||
public function create_list($prop)
|
||||
{
|
||||
$result = $this->rc->db->query(
|
||||
"INSERT INTO " . $this->db_lists . "
|
||||
(user_id, name, color, showalarms)
|
||||
VALUES (?, ?, ?, ?)",
|
||||
$this->rc->user->ID,
|
||||
$prop['name'],
|
||||
$prop['color'],
|
||||
$prop['showalarms']?1:0
|
||||
);
|
||||
|
||||
if ($result)
|
||||
return $this->rc->db->insert_id($this->sequence_lists);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update properties of an existing tasklist
|
||||
*
|
||||
* @param array Hash array with list properties
|
||||
* @return boolean True on success, Fales on failure
|
||||
* @see tasklist_driver::edit_list()
|
||||
*/
|
||||
public function edit_list($prop)
|
||||
{
|
||||
$query = $this->rc->db->query(
|
||||
"UPDATE " . $this->db_lists . "
|
||||
SET name=?, color=?, showalarms=?
|
||||
WHERE calendar_id=?
|
||||
AND user_id=?",
|
||||
$prop['name'],
|
||||
$prop['color'],
|
||||
$prop['showalarms']?1:0,
|
||||
$prop['id'],
|
||||
$this->rc->user->ID
|
||||
);
|
||||
|
||||
return $this->rc->db->affected_rows($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active/subscribed state of a list
|
||||
*
|
||||
* @param array Hash array with list properties
|
||||
* @return boolean True on success, Fales on failure
|
||||
* @see tasklist_driver::subscribe_list()
|
||||
*/
|
||||
public function subscribe_list($prop)
|
||||
{
|
||||
$hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', '')));
|
||||
|
||||
if ($prop['active'])
|
||||
unset($hidden[$prop['id']]);
|
||||
else
|
||||
$hidden[$prop['id']] = 1;
|
||||
|
||||
return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given list with all its contents
|
||||
*
|
||||
* @param array Hash array with list properties
|
||||
* @return boolean True on success, Fales on failure
|
||||
* @see tasklist_driver::remove_list()
|
||||
*/
|
||||
public function remove_list($prop)
|
||||
{
|
||||
// TODO: implement this
|
||||
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|today|tomorrow|overdue|nodate)
|
||||
* @see tasklist_driver::count_tasks()
|
||||
*/
|
||||
function count_tasks($lists = null)
|
||||
{
|
||||
if (empty($lists))
|
||||
$lists = array_keys($this->lists);
|
||||
else if (is_string($lists))
|
||||
$lists = explode(',', $lists);
|
||||
|
||||
// only allow to select from lists of this user
|
||||
$list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->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');
|
||||
|
||||
$result = $this->rc->db->query(sprintf(
|
||||
"SELECT task_id, flagged, date FROM " . $this->db_tasks . "
|
||||
WHERE tasklist_id IN (%s)
|
||||
AND del=0 AND complete<1",
|
||||
join(',', $list_ids)
|
||||
));
|
||||
|
||||
$counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
|
||||
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
|
||||
$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 wiht filter criterias
|
||||
* @param array List of lists to get tasks from
|
||||
* @return array List of tasks records matchin the criteria
|
||||
* @see tasklist_driver::list_tasks()
|
||||
*/
|
||||
function list_tasks($filter, $lists = null)
|
||||
{
|
||||
if (empty($lists))
|
||||
$lists = array_keys($this->lists);
|
||||
else if (is_string($lists))
|
||||
$lists = explode(',', $lists);
|
||||
|
||||
// only allow to select from lists of this user
|
||||
$list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
|
||||
$sql_add = '';
|
||||
|
||||
// add filter criteria
|
||||
if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
|
||||
$sql_add .= ' AND (date IS NULL OR date >= ?)';
|
||||
$datefrom = $filter['from'];
|
||||
}
|
||||
if ($filter['to']) {
|
||||
if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE)
|
||||
$sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter['to']) . ')';
|
||||
else
|
||||
$sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter['to']) . ')';
|
||||
}
|
||||
|
||||
// special case 'today': also show all events with date before today
|
||||
if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) {
|
||||
$datefrom = date('Y-m-d', 0);
|
||||
}
|
||||
|
||||
if ($filter['mask'] & tasklist::FILTER_MASK_NODATE)
|
||||
$sql_add = ' AND date IS NULL';
|
||||
|
||||
if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
|
||||
$sql_add .= ' AND complete=1';
|
||||
else // don't show complete tasks by default
|
||||
$sql_add .= ' AND complete<1';
|
||||
|
||||
if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED)
|
||||
$sql_add .= ' AND flagged=1';
|
||||
|
||||
// compose (slow) SQL query for searching
|
||||
// FIXME: improve searching using a dedicated col and normalized values
|
||||
if ($filter['search']) {
|
||||
$sql_query = array();
|
||||
foreach (array('title','description','organizer','attendees') as $col)
|
||||
$sql_query[] = $this->rc->db->ilike($col, '%'.$filter['search'].'%');
|
||||
$sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
|
||||
}
|
||||
|
||||
$tasks = array();
|
||||
if (!empty($list_ids)) {
|
||||
$datecol = $this->rc->db->quote_identifier('date');
|
||||
$timecol = $this->rc->db->quote_identifier('time');
|
||||
$result = $this->rc->db->query(sprintf(
|
||||
"SELECT * FROM " . $this->db_tasks . "
|
||||
WHERE tasklist_id IN (%s)
|
||||
AND del=0
|
||||
%s",
|
||||
join(',', $list_ids),
|
||||
$sql_add
|
||||
),
|
||||
$datefrom
|
||||
);
|
||||
|
||||
while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
|
||||
$tasks[] = $this->_read_postprocess($rec);
|
||||
}
|
||||
}
|
||||
|
||||
return $tasks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if (is_string($prop))
|
||||
$prop['uid'] = $prop;
|
||||
|
||||
$query_col = $prop['id'] ? 'task_id' : 'uid';
|
||||
|
||||
$result = $this->rc->db->query(sprintf(
|
||||
"SELECT * FROM " . $this->db_tasks . "
|
||||
WHERE tasklist_id IN (%s)
|
||||
AND %s=?
|
||||
AND del=0",
|
||||
$this->list_ids,
|
||||
$query_col
|
||||
),
|
||||
$prop['id'] ? $prop['id'] : $prop['uid']
|
||||
);
|
||||
|
||||
if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
|
||||
return $this->_read_postprocess($rec);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map some internal database values to match the generic "API"
|
||||
*/
|
||||
private function _read_postprocess($rec)
|
||||
{
|
||||
$rec['id'] = $rec['task_id'];
|
||||
$rec['list'] = $rec['tasklist_id'];
|
||||
$rec['changed'] = strtotime($rec['changed']);
|
||||
|
||||
if (!$rec['parent_id'])
|
||||
unset($rec['parent_id']);
|
||||
|
||||
unset($rec['task_id'], $rec['tasklist_id'], $rec['created']);
|
||||
return $rec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @see tasklist_driver::create_task()
|
||||
*/
|
||||
public function create_task($prop)
|
||||
{
|
||||
// check list permissions
|
||||
$list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists));
|
||||
if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly'])
|
||||
return false;
|
||||
|
||||
foreach (array('parent_id', 'date', 'time') as $col) {
|
||||
if (empty($prop[$col]))
|
||||
$prop[$col] = null;
|
||||
}
|
||||
|
||||
$result = $this->rc->db->query(sprintf(
|
||||
"INSERT INTO " . $this->db_tasks . "
|
||||
(tasklist_id, uid, parent_id, created, changed, title, date, time)
|
||||
VALUES (?, ?, ?, %s, %s, ?, ?, ?)",
|
||||
$this->rc->db->now(),
|
||||
$this->rc->db->now()
|
||||
),
|
||||
$list_id,
|
||||
$prop['uid'],
|
||||
$prop['parent_id'],
|
||||
$prop['title'],
|
||||
$prop['date'],
|
||||
$prop['time']
|
||||
);
|
||||
|
||||
if ($result)
|
||||
return $this->rc->db->insert_id($this->sequence_tasks);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an task entry with the given data
|
||||
*
|
||||
* @param array Hash array with task properties
|
||||
* @return boolean True on success, False on error
|
||||
* @see tasklist_driver::edit_task()
|
||||
*/
|
||||
public function edit_task($prop)
|
||||
{
|
||||
$sql_set = array();
|
||||
foreach (array('title', 'description', 'flagged', 'complete') as $col) {
|
||||
if (isset($prop[$col]))
|
||||
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]);
|
||||
}
|
||||
foreach (array('parent_id', 'date', 'time') as $col) {
|
||||
if (isset($prop[$col]))
|
||||
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col]));
|
||||
}
|
||||
|
||||
$query = $this->rc->db->query(sprintf(
|
||||
"UPDATE " . $this->db_tasks . "
|
||||
SET changed=%s %s
|
||||
WHERE task_id=?
|
||||
AND tasklist_id IN (%s)",
|
||||
$this->rc->db->now(),
|
||||
($sql_set ? ', ' . join(', ', $sql_set) : ''),
|
||||
$this->list_ids
|
||||
),
|
||||
$prop['id']
|
||||
);
|
||||
|
||||
return $this->rc->db->affected_rows($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a single task from the database
|
||||
*
|
||||
* @param array Hash array with task properties
|
||||
* @param boolean Remove record irreversible
|
||||
* @return boolean True on success, False on error
|
||||
* @see tasklist_driver::delete_task()
|
||||
*/
|
||||
public function delete_task($prop, $force = true)
|
||||
{
|
||||
$task_id = $prop['id'];
|
||||
|
||||
if ($task_id && $force) {
|
||||
$query = $this->rc->db->query(
|
||||
"DELETE FROM " . $this->db_tasks . "
|
||||
WHERE task_id=?
|
||||
AND tasklist_id IN (" . $this->list_ids . ")",
|
||||
$task_id
|
||||
);
|
||||
}
|
||||
else if ($task_id) {
|
||||
$query = $this->rc->db->query(sprintf(
|
||||
"UPDATE " . $this->db_tasks . "
|
||||
SET changed=%s, del=1
|
||||
WHERE task_id=?
|
||||
AND tasklist_id IN (%s)",
|
||||
$this->rc->db->now(),
|
||||
$this->list_ids
|
||||
),
|
||||
$task_id
|
||||
);
|
||||
}
|
||||
|
||||
return $this->rc->db->affected_rows($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a single deleted task (if supported)
|
||||
*
|
||||
* @param array Hash array with task properties
|
||||
* @return boolean True on success, False on error
|
||||
* @see tasklist_driver::undelete_task()
|
||||
*/
|
||||
public function undelete_task($prop)
|
||||
{
|
||||
$query = $this->rc->db->query(sprintf(
|
||||
"UPDATE " . $this->db_tasks . "
|
||||
SET changed=%s, del=0
|
||||
WHERE task_id=?
|
||||
AND tasklist_id IN (%s)",
|
||||
$this->rc->db->now(),
|
||||
$this->list_ids
|
||||
),
|
||||
$prop['id']
|
||||
);
|
||||
|
||||
return $this->rc->db->affected_rows($query);
|
||||
}
|
||||
|
||||
}
|
422
plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
Normal file
422
plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
Normal file
|
@ -0,0 +1,422 @@
|
|||
<?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 $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);
|
||||
|
||||
foreach ($names as $utf7name => $name) {
|
||||
$folder = $this->folders[$utf7name];
|
||||
$tasklist = array(
|
||||
'id' => kolab_storage::folder_id($utf7name),
|
||||
'name' => kolab_storage::object_name($utf7name),
|
||||
'color' => 'CC0000',
|
||||
'showalarms' => false,
|
||||
'active' => 1, #$folder->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION),
|
||||
);
|
||||
$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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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','!~','complete'))) as $record) {
|
||||
$rec = $this->_to_rcube_task($record);
|
||||
|
||||
if ($rec['complete']) // 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','~','complete');
|
||||
else
|
||||
$query[] = array('tags','!~','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'],
|
||||
'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');
|
||||
}
|
||||
if (is_a($record['dtstamp'], 'DateTime')) {
|
||||
$task['changed'] = $record['dtstamp']->format('U');
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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']);
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
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'];
|
||||
$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;
|
||||
}
|
||||
|
||||
|
||||
}
|
184
plugins/tasklist/drivers/tasklist_driver.php
Normal file
184
plugins/tasklist/drivers/tasklist_driver.php
Normal file
|
@ -0,0 +1,184 @@
|
|||
<?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' => <unixtime>, // Last modification date of record
|
||||
* 'title' => 'Event title/summary',
|
||||
* 'description' => 'Event description',
|
||||
* '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
|
||||
* '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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
43
plugins/tasklist/localization/de_CH.inc
Normal file
43
plugins/tasklist/localization/de_CH.inc
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
$labels = array();
|
||||
$labels['navtitle'] = 'Aufgaben';
|
||||
$labels['lists'] = 'Ressourcen';
|
||||
$labels['list'] = 'Ressource';
|
||||
|
||||
$labels['createnewtask'] = 'Neue Aufgabe eingeben';
|
||||
$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['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['taskdetails'] = 'Details';
|
||||
$labels['newtask'] = 'Neue Aufgabe';
|
||||
$labels['edittask'] = 'Aufgabe bearbeiten';
|
||||
$labels['save'] = 'Speichern';
|
||||
$labels['cancel'] = 'Abbrechen';
|
||||
$labels['addsubtask'] = 'Neue Teilaufgabe';
|
||||
|
||||
// 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.';
|
43
plugins/tasklist/localization/en_US.inc
Normal file
43
plugins/tasklist/localization/en_US.inc
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
$labels = array();
|
||||
$labels['navtitle'] = 'Tasks';
|
||||
$labels['lists'] = 'Resources';
|
||||
$labels['list'] = 'Resource';
|
||||
|
||||
$labels['createnewtask'] = 'Create new Task';
|
||||
$labels['mark'] = 'Mark';
|
||||
$labels['unmark'] = 'Unmark';
|
||||
$labels['edit'] = 'Edit';
|
||||
$labels['delete'] = 'Delete';
|
||||
$labels['title'] = 'Title';
|
||||
$labels['description'] = 'Description';
|
||||
$labels['datetime'] = 'Date/Time';
|
||||
|
||||
$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['taskdetails'] = 'Details';
|
||||
$labels['newtask'] = 'New Task';
|
||||
$labels['edittask'] = 'Edit Task';
|
||||
$labels['save'] = 'Save';
|
||||
$labels['cancel'] = 'Cancel';
|
||||
$labels['addsubtask'] = 'Add subtask';
|
||||
|
||||
// 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';
|
BIN
plugins/tasklist/skins/larry/sprites.png
Normal file
BIN
plugins/tasklist/skins/larry/sprites.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
plugins/tasklist/skins/larry/taskbaricon.png
Normal file
BIN
plugins/tasklist/skins/larry/taskbaricon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
504
plugins/tasklist/skins/larry/tasklist.css
Normal file
504
plugins/tasklist/skins/larry/tasklist.css
Normal file
|
@ -0,0 +1,504 @@
|
|||
/**
|
||||
* 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(taskbaricon.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 -44px;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
body.tasklistview #searchmenulink {
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
#selectorbox {
|
||||
position: absolute;
|
||||
top: 42px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 242px;
|
||||
}
|
||||
|
||||
#tasklistsbox {
|
||||
position: absolute;
|
||||
top: 300px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
#taskselector li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#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.selected {
|
||||
background-color: #c7e3ef;
|
||||
}
|
||||
|
||||
#taskselector li.overdue a {
|
||||
color: #b72a2a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#taskselector li.inactive a {
|
||||
color: #97b3bf;
|
||||
}
|
||||
|
||||
#taskselector li .count {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 6px;
|
||||
min-width: 1.8em;
|
||||
padding: 2px 4px;
|
||||
background: #d9ecf4;
|
||||
background: -moz-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9ecf4), color-stop(100%,#c7e3ef));
|
||||
background: -o-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
|
||||
background: -ms-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
|
||||
background: linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
|
||||
box-shadow: inset 0 1px 1px 0 #b7d3df;
|
||||
-o-box-shadow: inset 0 1px 1px 0 #b7d3df;
|
||||
-webkit-box-shadow: inset 0 1px 1px 0 #b7d3df;
|
||||
-moz-box-shadow: inset 0 1px 1px 0 #b7d3df;
|
||||
border: 1px solid #a7c3cf;
|
||||
border-radius: 9px;
|
||||
color: #69939e;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
#taskselector li.selected .count {
|
||||
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;
|
||||
}
|
||||
|
||||
#taskselector li.overdue.selected .count {
|
||||
background: #db3333;
|
||||
background: -moz-linear-gradient(top, #db3333 0%, #a82727 100%);
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#db3333), color-stop(100%,#a82727));
|
||||
background: -o-linear-gradient(top, #db3333 0%, #a82727 100%);
|
||||
background: -ms-linear-gradient(top, #db3333 0%, #a82727 100%);
|
||||
background: linear-gradient(top, #db3333 0%, #a82727 100%);
|
||||
box-shadow: inset 0 1px 1px 0 #831f1f;
|
||||
-o-box-shadow: inset 0 1px 1px 0 #831f1f;
|
||||
-webkit-box-shadow: inset 0 1px 1px 0 #831f1f;
|
||||
-moz-box-shadow: inset 0 1px 1px 0 #831f1f;
|
||||
border-color: #831f1f;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
#tasklists li.selected {
|
||||
background-color: #c7e3ef;
|
||||
}
|
||||
|
||||
#tasklists li.selected span.calname {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#mainview-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 256px;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#taskstoolbar {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: 0;
|
||||
width: 40%;
|
||||
height: 40px;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#quickaddbox {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 60%;
|
||||
height: 32px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#quickaddinput {
|
||||
width: 85%;
|
||||
margin: 0;
|
||||
padding: 5px 8px;
|
||||
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.3);
|
||||
}
|
||||
|
||||
#message.statusbar {
|
||||
border-top: 1px solid #c3c3c3;
|
||||
}
|
||||
|
||||
#tasksview .scroller {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 28px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#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: 11em;
|
||||
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 .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: 3px;
|
||||
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;
|
||||
}
|
||||
|
||||
#taskshow h2 {
|
||||
margin-top: -0.5em;
|
||||
}
|
||||
|
||||
#taskshow label {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
a.morelink {
|
||||
font-size: 90%;
|
||||
color: #0069a6;
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a.morelink:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#taskeditform input.text,
|
||||
#taskeditform textarea {
|
||||
width: 97%;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
141
plugins/tasklist/skins/larry/templates/mainview.html
Normal file
141
plugins/tasklist/skins/larry/templates/mainview.html
Normal file
|
@ -0,0 +1,141 @@
|
|||
<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="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="selectorbox" class="uibox listbox">
|
||||
<div class="scroller">
|
||||
<ul id="taskselector" class="listing">
|
||||
<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>
|
||||
|
||||
<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="taskstoolbar" class="toolbar">
|
||||
<roundcube:container name="toolbar" id="taskstoolbar" />
|
||||
</div>
|
||||
|
||||
<div id="tasksview" class="uibox">
|
||||
<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="taskshow">
|
||||
<div class="form-section">
|
||||
<h2 id="task-title"></h2>
|
||||
</div>
|
||||
<div id="task-description" class="form-section">
|
||||
</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-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>
|
||||
|
||||
<div id="taskedit">
|
||||
<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="40" />
|
||||
</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="40"></textarea>
|
||||
</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" />
|
||||
<input type="text" name="time" size="6" id="edit-time" />
|
||||
<a href="#nodate" style="margin-left:1em" id="edit-nodate"><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" /> %
|
||||
<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" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
// UI startup
|
||||
var UI = new rcube_mail_ui();
|
||||
|
||||
$(document).ready(function(e){
|
||||
UI.init();
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
872
plugins/tasklist/tasklist.js
Normal file
872
plugins/tasklist/tasklist.js
Normal file
|
@ -0,0 +1,872 @@
|
|||
/**
|
||||
* 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(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 filtermask = FILTER_MASK_ALL;
|
||||
var idcount = 0;
|
||||
var selected_list;
|
||||
var saving_lock;
|
||||
var ui_loading;
|
||||
var taskcounts = {};
|
||||
var listdata = {};
|
||||
var draghelper;
|
||||
var completeness_slider;
|
||||
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;
|
||||
|
||||
/* 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;
|
||||
|
||||
|
||||
/**
|
||||
* initialize the tasks UI
|
||||
*/
|
||||
function init()
|
||||
{
|
||||
// select the first task list
|
||||
for (var s in me.tasklists) {
|
||||
selected_list = s;
|
||||
break;
|
||||
};
|
||||
|
||||
// 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.reload_data', function(){ list_tasks(null); });
|
||||
rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, saving_lock); });
|
||||
|
||||
// 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:selected_list }, 'new');
|
||||
render_task(rec);
|
||||
|
||||
// clear form
|
||||
this.reset();
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
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)) });
|
||||
|
||||
// handle global document clicks: close popup menus
|
||||
$(document.body).click(clear_popups);
|
||||
|
||||
// extended datepicker settings
|
||||
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()
|
||||
{
|
||||
rcmail.http_request('counts');
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch tasks from server
|
||||
*/
|
||||
function list_tasks(sel)
|
||||
{
|
||||
if (sel && filter_masks[sel] !== undefined) {
|
||||
filtermask = filter_masks[sel];
|
||||
selector = sel;
|
||||
}
|
||||
|
||||
ui_loading = rcmail.set_busy(true, 'loading');
|
||||
rcmail.http_request('fetch', { filter:filtermask, q:search_query }, true);
|
||||
|
||||
$('#taskselector li.selected').removeClass('selected');
|
||||
$('#taskselector li.'+selector).addClass('selected');
|
||||
}
|
||||
|
||||
/**
|
||||
* callback if task data from server is ready
|
||||
*/
|
||||
function data_ready(data)
|
||||
{
|
||||
// clear display
|
||||
var msgbox = $('#listmessagebox').hide(),
|
||||
list = $(rcmail.gui_objects.resultlist).html('');
|
||||
listdata = {};
|
||||
|
||||
for (var i=0; i < data.length; i++) {
|
||||
listdata[data[i].id] = data[i];
|
||||
render_task(data[i]);
|
||||
}
|
||||
|
||||
if (!data.length)
|
||||
msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
|
||||
|
||||
rcmail.set_busy(false, 'loading', ui_loading);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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;
|
||||
listdata[id] = rec;
|
||||
render_task(rec, rec.tempid || id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('task', { action:action, t:rec, filter:filtermask });
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the given task into the tasks list
|
||||
*/
|
||||
function render_task(rec, replace)
|
||||
{
|
||||
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="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');
|
||||
console.log(replace)
|
||||
var li, parent;
|
||||
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 (rec.parent_id && (parent = $('li[rel="'+rec.parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist)) && parent.length)
|
||||
li.appendTo(parent);
|
||||
else
|
||||
li.appendTo(rcmail.gui_objects.resultlist);
|
||||
}
|
||||
|
||||
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, 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);
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
});
|
||||
|
||||
if (next_li) {
|
||||
if (animated) insert_animated(li, null, next_li);
|
||||
else li.insertAfter(next_li);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'),
|
||||
rec = listdata[parent_id];
|
||||
|
||||
if (parent_id == listdata[drag_id].parent_id)
|
||||
return false;
|
||||
|
||||
while (rec && rec.parent_id) {
|
||||
if (rec.parent_id == drag_id)
|
||||
return false;
|
||||
rec = listdata[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-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-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
|
||||
$('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
|
||||
|
||||
// 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);
|
||||
},
|
||||
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] :
|
||||
(selected_list ? me.tasklists[selected_list] : { editable: action=='new' });
|
||||
|
||||
if (list.readonly || (action == 'edit' && (!rec || rec.readonly || rec.temp)))
|
||||
return false;
|
||||
|
||||
me.selected_task = $.extend({}, rec); // clone task object
|
||||
|
||||
// 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 complete = $('#edit-completeness').val((rec.complete || 0) * 100);
|
||||
completeness_slider.slider('value', complete.val());
|
||||
var tasklist = $('#edit-tasklist').val(rec.list || 0);
|
||||
|
||||
$('#edit-nodate').unbind('click').click(function(){
|
||||
recdate.val('');
|
||||
rectime.val('');
|
||||
return false;
|
||||
})
|
||||
|
||||
// define dialog buttons
|
||||
var buttons = {};
|
||||
buttons[rcmail.gettext('save', 'tasklist')] = function() {
|
||||
me.selected_task.title = title.val();
|
||||
me.selected_task.description = description.val();
|
||||
me.selected_task.date = recdate.val();
|
||||
me.selected_task.time = rectime.val();
|
||||
me.selected_task.list = tasklist.val();
|
||||
|
||||
if (me.selected_task.list && me.selected_task.list != 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();
|
||||
},
|
||||
buttons: buttons,
|
||||
minWidth: 500,
|
||||
width: 580
|
||||
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
|
||||
|
||||
title.select();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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
|
||||
*/
|
||||
function match_filter(rec)
|
||||
{
|
||||
// TBD.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
$('.popupmenu:visible').each(function(i, elem){
|
||||
var menu = $(elem);
|
||||
if (!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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* tasklist plugin UI initialization */
|
||||
var rctasks;
|
||||
window.rcmail && rcmail.addEventListener('init', function(evt) {
|
||||
|
||||
rctasks = new rcube_tasklist(rcmail.env.tasklist_settings);
|
||||
|
||||
// register button commands
|
||||
//rcmail.register_command('addtask', function(){ tasks.add_task(); }, true);
|
||||
//rcmail.register_command('print', function(){ tasks.print_list(); }, true);
|
||||
|
||||
rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
|
||||
rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
|
||||
|
||||
rctasks.init();
|
||||
});
|
485
plugins/tasklist/tasklist.php
Normal file
485
plugins/tasklist/tasklist.php
Normal file
|
@ -0,0 +1,485 @@
|
|||
<?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'));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
switch ($action) {
|
||||
|
||||
}
|
||||
|
||||
if ($success)
|
||||
$this->rc->output->show_message('successfullysaved', 'confirmation');
|
||||
else
|
||||
$this->rc->output->show_message('tasklist.errorsaving', 'error');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function fetch_counts()
|
||||
{
|
||||
$lists = null;
|
||||
$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 = null;
|
||||
|
||||
// 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 = $this->task_tree = $this->tasks_childs = array();
|
||||
foreach ($this->driver->list_tasks($filter, $lists) as $rec) {
|
||||
if ($rec['parent_id']) {
|
||||
$this->tasks_childs[$rec['parent_id']]++;
|
||||
$this->task_tree[$rec['id']] = $rec['parent_id'];
|
||||
}
|
||||
$this->encode_task($rec);
|
||||
|
||||
// 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', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
|
||||
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 ($this->tasks_childs[$rec['id']])
|
||||
$rec['_haschilds'] = $this->tasks_childs[$rec['id']];
|
||||
|
||||
if (!isset($rec['_depth'])) {
|
||||
$rec['_depth'] = 0;
|
||||
$parent_id = $this->task_tree[$rec['id']];
|
||||
while ($parent_id) {
|
||||
$rec['_depth']++;
|
||||
$parent_id = $this->task_tree[$parent_id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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_TODAY;
|
||||
else if ($rec['date'] == $tomorrow)
|
||||
$mask |= self::FILTER_MASK_TOMORROW;
|
||||
else if ($rec['date'] < $today)
|
||||
$mask |= self::FILTER_MASK_OVERDUE;
|
||||
else if ($rec['date'] > $tomorrow && $rec['date'] <= $weeklimit)
|
||||
$mask |= self::FILTER_MASK_LATER;
|
||||
else if ($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');
|
||||
}
|
||||
|
||||
|
||||
/******* 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
181
plugins/tasklist/tasklist_ui.php
Normal file
181
plugins/tasklist/tasklist_ui.php
Normal file
|
@ -0,0 +1,181 @@
|
|||
<?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');
|
||||
|
||||
$skin = $this->rc->config->get('skin');
|
||||
$this->plugin->include_stylesheet('skins/' . $skin . '/tasklist.css');
|
||||
$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.tasks', array($this, 'tasks_resultview'));
|
||||
|
||||
$this->plugin->include_script('tasklist.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);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
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;
|
||||
$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'], 'disabled' => true)) .
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, '');
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue