From 7f5a849e7816e7b4c7b13a72d38a9c777632d7cd Mon Sep 17 00:00:00 2001 From: alecpl Date: Thu, 7 Jul 2011 11:44:26 +0000 Subject: - Added possibility to undo last contact delete operation --- CHANGELOG | 1 + program/include/rcmail.php | 2 +- program/include/rcube_addressbook.php | 13 ++++- program/include/rcube_contacts.php | 34 ++++++++++++- program/include/rcube_json_output.php | 5 +- program/include/rcube_template.php | 13 ++--- program/js/app.js | 26 ++++++---- program/localization/en_US/labels.inc | 1 + program/localization/en_US/messages.inc | 3 ++ program/localization/pl_PL/labels.inc | 1 + program/localization/pl_PL/messages.inc | 3 ++ program/steps/addressbook/delete.inc | 22 ++++++++- program/steps/addressbook/func.inc | 6 +++ program/steps/addressbook/undo.inc | 87 +++++++++++++++++++++++++++++++++ skins/default/common.css | 6 +++ skins/default/templates/mail.html | 2 +- 16 files changed, 201 insertions(+), 24 deletions(-) create mode 100644 program/steps/addressbook/undo.inc diff --git a/CHANGELOG b/CHANGELOG index 27de618be..8748ecc54 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Added possibility to undo last contact delete operation - Fix sorting of contact groups after group create (#1487747) - Add optional textual upload progress indicator (#1486039) - Fix parsing URLs containing commas (#1487970) diff --git a/program/include/rcmail.php b/program/include/rcmail.php index e94e205e7..fe2d99475 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -507,7 +507,7 @@ class rcmail $this->output->set_env('comm_path', $this->comm_path); $this->output->set_charset(RCMAIL_CHARSET); - // add some basic label to client + // add some basic labels to client $this->output->add_label('loading', 'servererror'); return $this->output; diff --git a/program/include/rcube_addressbook.php b/program/include/rcube_addressbook.php index cefe4612f..3581b83d1 100644 --- a/program/include/rcube_addressbook.php +++ b/program/include/rcube_addressbook.php @@ -38,6 +38,7 @@ abstract class rcube_addressbook public $primary_key; public $groups = false; public $readonly = true; + public $undelete = false; public $ready = false; public $group_id = null; public $list_page = 1; @@ -256,7 +257,17 @@ abstract class rcube_addressbook } /** - * Remove all records from the database + * Unmark delete flag on contact record(s) + * + * @param array Record identifiers + */ + function undelete($ids) + { + /* empty for read-only address books */ + } + + /** + * Mark all records in database as deleted */ function delete_all() { diff --git a/program/include/rcube_contacts.php b/program/include/rcube_contacts.php index b097b3bc0..52667fab3 100644 --- a/program/include/rcube_contacts.php +++ b/program/include/rcube_contacts.php @@ -52,6 +52,7 @@ class rcube_contacts extends rcube_addressbook public $primary_key = 'contact_id'; public $readonly = false; public $groups = true; + public $undelete = true; public $list_page = 1; public $page_size = 10; public $group_id = 0; @@ -691,13 +692,44 @@ class rcube_contacts extends rcube_addressbook } + /** + * Undelete one or more contact records + * + * @param array Record identifiers + */ + function undelete($ids) + { + if (!is_array($ids)) + $ids = explode(',', $ids); + + $ids = $this->db->array2list($ids, 'integer'); + + // flag record as deleted + $this->db->query( + "UPDATE ".get_table_name($this->db_name). + " SET del=0, changed=".$this->db->now(). + " WHERE user_id=?". + " AND contact_id IN ($ids)", + $this->user_id + ); + + $this->cache = null; + + return $this->db->affected_rows(); + } + + /** * Remove all records from the database */ function delete_all() { - $this->db->query("DELETE FROM ".get_table_name($this->db_name)." WHERE user_id = ?", $this->user_id); $this->cache = null; + + $this->db->query("UPDATE ".get_table_name($this->db_name). + " SET del=1, changed=".$this->db->now(). + " WHERE user_id = ?", $this->user_id); + return $this->db->affected_rows(); } diff --git a/program/include/rcube_json_output.php b/program/include/rcube_json_output.php index d1c9de279..efb672888 100644 --- a/program/include/rcube_json_output.php +++ b/program/include/rcube_json_output.php @@ -164,14 +164,15 @@ class rcube_json_output * @param string $type Message type [notice|confirm|error] * @param array $vars Key-value pairs to be replaced in localized text * @param boolean $override Override last set message + * @param int $timeout Message displaying time in seconds * @uses self::command() */ - public function show_message($message, $type='notice', $vars=null, $override=true) + public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0) { if ($override || !$this->message) { $this->message = $message; $msgtext = rcube_label_exists($message) ? rcube_label(array('name' => $message, 'vars' => $vars)) : $message; - $this->command('display_message', $msgtext, $type); + $this->command('display_message', $msgtext, $type, $timeout * 1000); } } diff --git a/program/include/rcube_template.php b/program/include/rcube_template.php index 39b22826a..a4c1a6915 100755 --- a/program/include/rcube_template.php +++ b/program/include/rcube_template.php @@ -238,18 +238,19 @@ class rcube_template extends rcube_html_page /** * Invoke display_message command * - * @param string Message to display - * @param string Message type [notice|confirm|error] - * @param array Key-value pairs to be replaced in localized text - * @param boolean Override last set message + * @param string $message Message to display + * @param string $type Message type [notice|confirm|error] + * @param array $vars Key-value pairs to be replaced in localized text + * @param boolean $override Override last set message + * @param int $timeout Message display time in seconds * @uses self::command() */ - public function show_message($message, $type='notice', $vars=null, $override=true) + public function show_message($message, $type='notice', $vars=null, $override=true, $timeout=0) { if ($override || !$this->message) { $this->message = $message; $msgtext = rcube_label_exists($message) ? rcube_label(array('name' => $message, 'vars' => $vars)) : $message; - $this->command('display_message', $msgtext, $type); + $this->command('display_message', $msgtext, $type, $timeout * 1000); } } diff --git a/program/js/app.js b/program/js/app.js index 6a35c596a..125b6450c 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -164,7 +164,7 @@ function rcube_webmail() } // enable general commands - this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', true); + this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', 'undo', true); if (this.env.permaurl) this.enable_command('permaurl', true); @@ -411,7 +411,7 @@ function rcube_webmail() // show message if (this.pending_message) - this.display_message(this.pending_message[0], this.pending_message[1]); + this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]); // map implicit containers if (this.gui_objects.folderlist) @@ -1046,6 +1046,10 @@ function rcube_webmail() this.goto_url('settings/' + command); break; + case 'undo': + this.http_request('undo', '', this.display_message('', 'loading')); + break; + // unified command call (command name == function name) default: var func = command.replace(/-/g, '_'); @@ -1296,7 +1300,7 @@ function rcube_webmail() var toffset = -moffset-boffset; var li, div, pos, mouse, check, oldclass, layerclass = 'draglayernormal'; - + if (this.contact_list && this.contact_list.draglayer) oldclass = this.contact_list.draglayer.attr('class'); @@ -4980,7 +4984,7 @@ function rcube_webmail() if (elem._placeholder && (!$elem.val() || $elem.val() == elem._placeholder)) $elem.addClass('placeholder').attr('spellcheck', false).val(elem._placeholder); }; - + // write to the document/window title this.set_pagetitle = function(title) { @@ -4989,27 +4993,29 @@ function rcube_webmail() }; // display a system message, list of types in common.css (below #message definition) - this.display_message = function(msg, type) + this.display_message = function(msg, type, timeout) { // pass command to parent window if (this.is_framed()) - return parent.rcmail.display_message(msg, type); + return parent.rcmail.display_message(msg, type, timeout); if (!this.gui_objects.message) { // save message in order to display after page loaded if (type != 'loading') - this.pending_message = new Array(msg, type); + this.pending_message = new Array(msg, type, timeout); return false; } type = type ? type : 'notice'; var ref = this, - key = msg, + key = String(msg).replace(this.identifier_expr, '_'), date = new Date(), - id = type + date.getTime(), + id = type + date.getTime(); + + if (!timeout) timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1); - + if (type == 'loading') { key = 'loading'; timeout = this.env.request_timeout * 1000; diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index b2f136b71..60676aae3 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -448,6 +448,7 @@ $labels['sharedfolder'] = 'Public Folder'; $labels['sortby'] = 'Sort by'; $labels['sortasc'] = 'Sort ascending'; $labels['sortdesc'] = 'Sort descending'; +$labels['undo'] = 'Undo'; // units $labels['B'] = 'B'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index da4b41339..e64b2298c 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -128,6 +128,8 @@ $messages['maxgroupmembersreached'] = 'The number of group members exceeds the m $messages['internalerror'] = 'An internal error occured. Please try again'; $messages['contactdelerror'] = 'Could not delete contact(s)'; $messages['contactdeleted'] = 'Contact(s) deleted successfully'; +$messages['contactrestoreerror'] = 'Could not restore deleted contact(s)'; +$messages['contactrestored'] = 'Contact(s) restored successfully'; $messages['groupdeleted'] = 'Group deleted successfully'; $messages['grouprenamed'] = 'Group renamed successfully'; $messages['groupcreated'] = 'Group created successfully'; @@ -142,5 +144,6 @@ $messages['folderupdated'] = 'Folder updated successfully'; $messages['foldercreated'] = 'Folder created successfully'; $messages['invalidimageformat'] = 'Not a valid image format'; $messages['mispellingsfound'] = 'Spelling errors detected in the message'; +$messages['itemsdeleted'] = '$num item(s) has been deleted.'; ?> diff --git a/program/localization/pl_PL/labels.inc b/program/localization/pl_PL/labels.inc index adb33e9d0..aa7127d04 100644 --- a/program/localization/pl_PL/labels.inc +++ b/program/localization/pl_PL/labels.inc @@ -416,5 +416,6 @@ $labels['other'] = 'Inne'; $labels['importtarget'] = 'Dodaj nowe kontakty do książki adresowej:'; $labels['grouprename'] = 'Zmień nazwę grupy'; $labels['groupdelete'] = 'Usuń grupę'; +$labels['undo'] = 'Cofnij'; ?> diff --git a/program/localization/pl_PL/messages.inc b/program/localization/pl_PL/messages.inc index 9c7c22a51..6a0524417 100644 --- a/program/localization/pl_PL/messages.inc +++ b/program/localization/pl_PL/messages.inc @@ -146,5 +146,8 @@ $messages['errornoperm'] = 'Nie można wykonać operacji. Brak uprawnień'; $messages['importconfirmskipped'] = 'Pominięto $skipped istniejących wpisów'; $messages['invalidimageformat'] = 'Niepoprawny format obrazka'; $messages['mispellingsfound'] = 'Wykryto błędy pisowni w tej wiadomości'; +$messages['itemsdeleted'] = '$num elemenów zostało usuniętych.'; +$messages['contactrestoreerror'] = 'Przywracanie kontaktów nie powiodło się'; +$messages['contactrestored'] = 'Kontakt(y) zostały przywrócone'; ?> diff --git a/program/steps/addressbook/delete.inc b/program/steps/addressbook/delete.inc index af9bdb10a..f11752b70 100644 --- a/program/steps/addressbook/delete.inc +++ b/program/steps/addressbook/delete.inc @@ -26,6 +26,9 @@ if (!$OUTPUT->ajax_call) $cids = rcmail_get_cids(); $delcnt = 0; +// remove previous deletes +$RCMAIL->session->remove('contact_undo'); + foreach ($cids as $source => $cid) { $CONTACTS = rcmail_contact_source($source); @@ -36,6 +39,7 @@ foreach ($cids as $source => $cid) if (count($cids) == 1) { $OUTPUT->show_message('contactdelerror', 'error'); $OUTPUT->command('list_contacts'); + $OUTPUT->send(); } continue; } @@ -52,11 +56,14 @@ foreach ($cids as $source => $cid) } else { $delcnt += $deleted; + + // store deleted contacts IDs in session for undelete + if ($CONTACTS->undelete) { + $_SESSION['contact_undo']['data'][$source] = $cid; + } } } -$OUTPUT->show_message('contactdeleted', 'confirmation'); - $page = isset($_SESSION['page']) ? $_SESSION['page'] : 1; // update saved search after data changed @@ -135,6 +142,17 @@ else { $OUTPUT->set_env('pagecount', ceil($result->count / $CONFIG['pagesize'])); $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result)); +if (!empty($_SESSION['contact_undo'])) { + $_SESSION['contact_undo']['ts'] = time(); + $msg = html::span(null, rcube_label(array('name' => 'itemsdeleted', 'vars' => array('num' => $deleted)))) + . ' ' . html::a(array('onclick' => JS_OBJECT_NAME.".command('undo', '', this)"), rcube_label('undo')); + + $OUTPUT->show_message($msg, 'confirmation', null, true, $RCMAIL->config->get('undo_timeout', 15)); +} +else { + $OUTPUT->show_message('contactdeleted', 'confirmation'); +} + // add new rows from next page (if any) if (!empty($records)) { rcmail_js_contacts_list($records); diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 4c930c80b..a895b617f 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -88,6 +88,12 @@ if (!$RCMAIL->action && !$OUTPUT->ajax_call) { $CONTACTS = rcmail_contact_source($source, true); } +// remove undo information... +if ($undo = $_SESSION['contact_undo']) { + // ...after 30 seconds + if ($undo['ts'] < time() - 30) + $RCMAIL->session->remove('contact_undo'); +} // instantiate a contacts object according to the given source function rcmail_contact_source($source=null, $init_env=false) diff --git a/program/steps/addressbook/undo.inc b/program/steps/addressbook/undo.inc new file mode 100644 index 000000000..936f11086 --- /dev/null +++ b/program/steps/addressbook/undo.inc @@ -0,0 +1,87 @@ + | + +-----------------------------------------------------------------------+ + + $Id$ + +*/ + +// process ajax requests only +if (!$OUTPUT->ajax_call) + return; + +$undo = $_SESSION['contact_undo']; +$delcnt = 0; + +foreach ((array)$undo['data'] as $source => $cid) +{ + $CONTACTS = rcmail_contact_source($source); + + $plugin = $RCMAIL->plugins->exec_hook('contact_undelete', array( + 'id' => $cid, 'source' => $source)); + + $restored = !$plugin['abort'] ? $CONTACTS->undelete($cid) : $plugin['result']; + + if (!$restored) { + $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'contactrestoreerror', 'error'); + $OUTPUT->command('list_contacts'); + $OUTPUT->send(); + } + else { + $delcnt += $restored; + } +} + +// update saved search after data changed +if ($delcnt && ($search_request = $_REQUEST['_search']) && isset($_SESSION['search'][$search_request])) { + $search = (array)$_SESSION['search'][$search_request]; + + foreach ($search as $s => $set) { + $source = $RCMAIL->get_address_book($s); + + // reset page + $source->set_page(1); + $source->set_pagesize(9999); + $source->set_search_set($set); + + // get records + $result = $source->list_records(array('name', 'email')); + + if (!$result->count) { + unset($search[$s]); + continue; + } + + while ($row = $result->next()) { + $row['sourceid'] = $s; + $key = $row['name'] . ':' . $row['sourceid']; + $records[$key] = $row; + } + unset($result); + + $search[$s] = $source->get_search_set(); + } + + $_SESSION['search'][$search_request] = $search; +} + +$RCMAIL->session->remove('contact_undo'); + +$OUTPUT->show_message('contactrestored', 'confirmation'); +$OUTPUT->command('list_contacts'); + +// send response +$OUTPUT->send(); diff --git a/skins/default/common.css b/skins/default/common.css index 4f1ab6b01..55d537cc4 100644 --- a/skins/default/common.css +++ b/skins/default/common.css @@ -252,6 +252,12 @@ img border: 1px solid #CCCCCC; } +#message a +{ + cursor: pointer; + text-decoration: underline; +} + .box { border: 1px solid #999; diff --git a/skins/default/templates/mail.html b/skins/default/templates/mail.html index ea6a2f71c..1645ba178 100644 --- a/skins/default/templates/mail.html +++ b/skins/default/templates/mail.html @@ -196,6 +196,6 @@ - +
11 item(s) has been deleted. Undo
-- cgit v1.2.3