From f8e48df71540b268ceac058d32b8ee848fc2ab6b Mon Sep 17 00:00:00 2001 From: alecpl Date: Tue, 6 Sep 2011 16:35:14 +0000 Subject: [PATCH] - Merge devel-saved_search branch (Addressbook Saved Searches) --- CHANGELOG | 1 + SQL/mssql.initial.sql | 29 ++++ SQL/mssql.upgrade.sql | 30 ++++ SQL/mysql.initial.sql | 16 ++ SQL/mysql.update.sql | 12 ++ SQL/postgres.initial.sql | 26 +++ SQL/postgres.update.sql | 16 ++ SQL/sqlite.initial.sql | 15 ++ SQL/sqlite.update.sql | 11 ++ config/db.inc.php.dist | 2 + program/include/rcube_user.php | 127 ++++++++++++++ program/js/app.js | 184 ++++++++++++++++----- program/localization/en_US/labels.inc | 6 +- program/localization/en_US/messages.inc | 6 +- program/steps/addressbook/func.inc | 29 +++- program/steps/addressbook/search.inc | 71 +++++++- skins/default/addressbook.css | 10 +- skins/default/images/icons/folders.gif | Bin 2430 -> 2441 bytes skins/default/images/icons/folders.png | Bin 4771 -> 5332 bytes skins/default/includes/messagetoolbar.html | 2 +- skins/default/templates/addressbook.html | 4 +- 21 files changed, 549 insertions(+), 48 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ecc67ce0b..4d6b3e8cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Addressbook Saved Searches - Added spellchecker exceptions dictionary (shared or per-user) - Added possibility to ignore words containing caps, numbers, symbols (spellcheck_ignore_* options) diff --git a/SQL/mssql.initial.sql b/SQL/mssql.initial.sql index 321da7cbb..8e103eb55 100644 --- a/SQL/mssql.initial.sql +++ b/SQL/mssql.initial.sql @@ -100,6 +100,15 @@ CREATE TABLE [dbo].[dictionary] ( ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO +CREATE TABLE [dbo].[searches] ( + [search_id] [int] IDENTITY (1, 1) NOT NULL , + [user_id] [int] NOT NULL , + [type] [tinyint] NOT NULL , + [name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL , + [data] [text] COLLATE Latin1_General_CI_AI NOT NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + ALTER TABLE [dbo].[cache] WITH NOCHECK ADD PRIMARY KEY CLUSTERED ( @@ -156,6 +165,13 @@ ALTER TABLE [dbo].[users] WITH NOCHECK ADD ) ON [PRIMARY] GO +ALTER TABLE [dbo].[searches] WITH NOCHECK ADD + CONSTRAINT [PK_searches_search_id] PRIMARY KEY CLUSTERED + ( + [search_id] + ) ON [PRIMARY] +GO + ALTER TABLE [dbo].[cache] ADD CONSTRAINT [DF_cache_user_id] DEFAULT ('0') FOR [user_id], CONSTRAINT [DF_cache_cache_key] DEFAULT ('') FOR [cache_key], @@ -274,6 +290,14 @@ GO CREATE UNIQUE INDEX [IX_dictionary_user_language] ON [dbo].[dictionary]([user_id],[language]) ON [PRIMARY] GO +ALTER TABLE [dbo].[searches] ADD + CONSTRAINT [DF_searches_user] DEFAULT (0) FOR [user_id], + CONSTRAINT [DF_searches_type] DEFAULT (0) FOR [type], +GO + +CREATE UNIQUE INDEX [IX_searches_user_type_name] ON [dbo].[searches]([user_id],[type],[name]) ON [PRIMARY] +GO + ALTER TABLE [dbo].[identities] ADD CONSTRAINT [FK_identities_user_id] FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id]) ON DELETE CASCADE ON UPDATE CASCADE @@ -304,6 +328,11 @@ ALTER TABLE [dbo].[contactgroupmembers] ADD CONSTRAINT [FK_contactgroupmembers_c ON DELETE CASCADE ON UPDATE CASCADE GO +ALTER TABLE [dbo].[searches] ADD CONSTRAINT [FK_searches_user_id] + FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id]) + ON DELETE CASCADE ON UPDATE CASCADE +GO + -- Use trigger instead of foreign key (#1487112) -- "Introducing FOREIGN KEY constraint ... may cause cycles or multiple cascade paths." CREATE TRIGGER [contact_delete_member] ON [dbo].[contacts] diff --git a/SQL/mssql.upgrade.sql b/SQL/mssql.upgrade.sql index a77362ac4..258b1d78a 100644 --- a/SQL/mssql.upgrade.sql +++ b/SQL/mssql.upgrade.sql @@ -121,3 +121,33 @@ CREATE TABLE [dbo].[dictionary] ( GO CREATE UNIQUE INDEX [IX_dictionary_user_language] ON [dbo].[dictionary]([user_id],[language]) ON [PRIMARY] GO + +CREATE TABLE [dbo].[searches] ( + [search_id] [int] IDENTITY (1, 1) NOT NULL , + [user_id] [int] NOT NULL , + [type] [tinyint] NOT NULL , + [name] [varchar] (128) COLLATE Latin1_General_CI_AI NOT NULL , + [data] [text] COLLATE Latin1_General_CI_AI NOT NULL +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + +ALTER TABLE [dbo].[searches] WITH NOCHECK ADD + CONSTRAINT [PK_searches_search_id] PRIMARY KEY CLUSTERED + ( + [search_id] + ) ON [PRIMARY] +GO + +ALTER TABLE [dbo].[searches] ADD + CONSTRAINT [DF_searches_user] DEFAULT (0) FOR [user_id], + CONSTRAINT [DF_searches_type] DEFAULT (0) FOR [type], +GO + +CREATE UNIQUE INDEX [IX_searches_user_type_name] ON [dbo].[searches]([user_id],[type],[name]) ON [PRIMARY] +GO + +ALTER TABLE [dbo].[searches] ADD CONSTRAINT [FK_searches_user_id] + FOREIGN KEY ([user_id]) REFERENCES [dbo].[users] ([user_id]) + ON DELETE CASCADE ON UPDATE CASCADE +GO + diff --git a/SQL/mysql.initial.sql b/SQL/mysql.initial.sql index 4cabb8132..6c2669009 100644 --- a/SQL/mysql.initial.sql +++ b/SQL/mysql.initial.sql @@ -155,4 +155,20 @@ CREATE TABLE `dictionary` ( UNIQUE `uniqueness` (`user_id`, `language`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; + +-- Table structure for table `searches` + +CREATE TABLE `searches` ( + `search_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0', + `type` int(3) NOT NULL DEFAULT '0', + `name` varchar(128) NOT NULL, + `data` text, + PRIMARY KEY(`search_id`), + CONSTRAINT `user_id_fk_searches` FOREIGN KEY (`user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE `uniqueness` (`user_id`, `type`, `name`) +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; + + /*!40014 SET FOREIGN_KEY_CHECKS=1 */; diff --git a/SQL/mysql.update.sql b/SQL/mysql.update.sql index ee3929c4c..afeb3a528 100644 --- a/SQL/mysql.update.sql +++ b/SQL/mysql.update.sql @@ -155,3 +155,15 @@ CREATE TABLE `dictionary` ( REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE `uniqueness` (`user_id`, `language`) ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; + +CREATE TABLE `searches` ( + `search_id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` int(10) UNSIGNED NOT NULL DEFAULT '0', + `type` int(3) NOT NULL DEFAULT '0', + `name` varchar(128) NOT NULL, + `data` text, + PRIMARY KEY(`search_id`), + CONSTRAINT `user_id_fk_searches` FOREIGN KEY (`user_id`) + REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE, + UNIQUE `uniqueness` (`user_id`, `type`, `name`) +) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */; diff --git a/SQL/postgres.initial.sql b/SQL/postgres.initial.sql index c801a773c..01221c4e3 100644 --- a/SQL/postgres.initial.sql +++ b/SQL/postgres.initial.sql @@ -238,3 +238,29 @@ CREATE TABLE dictionary ( data text NOT NULL, CONSTRAINT dictionary_user_id_language_key UNIQUE (user_id, "language") ); + +-- +-- Sequence "searches_ids" +-- Name: searches_ids; Type: SEQUENCE; Schema: public; Owner: postgres +-- + +CREATE SEQUENCE search_ids + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +-- +-- Table "searches" +-- Name: searches; Type: TABLE; Schema: public; Owner: postgres +-- + +CREATE TABLE searches ( + search_id integer DEFAULT nextval('search_ids'::text) PRIMARY KEY, + user_id integer NOT NULL + REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE, + "type" smallint DEFAULT 0 NOT NULL, + name varchar(128) NOT NULL, + data text NOT NULL, + CONSTRAINT searches_user_id_key UNIQUE (user_id, "type", name) +); diff --git a/SQL/postgres.update.sql b/SQL/postgres.update.sql index 0a2ed99dd..e316ff540 100644 --- a/SQL/postgres.update.sql +++ b/SQL/postgres.update.sql @@ -110,3 +110,19 @@ CREATE TABLE dictionary ( data text NOT NULL, CONSTRAINT dictionary_user_id_language_key UNIQUE (user_id, "language") ); + +CREATE SEQUENCE search_ids + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +CREATE TABLE searches ( + search_id integer DEFAULT nextval('search_ids'::text) PRIMARY KEY, + user_id integer NOT NULL + REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE, + "type" smallint DEFAULT 0 NOT NULL, + name varchar(128) NOT NULL, + data text NOT NULL, + CONSTRAINT searches_user_id_key UNIQUE (user_id, "type", name) +); diff --git a/SQL/sqlite.initial.sql b/SQL/sqlite.initial.sql index 337dfbe8d..46ee5301b 100644 --- a/SQL/sqlite.initial.sql +++ b/SQL/sqlite.initial.sql @@ -161,3 +161,18 @@ CREATE TABLE dictionary ( CREATE UNIQUE INDEX ix_dictionary_user_language ON dictionary (user_id, "language"); +-- -------------------------------------------------------- + +-- +-- Table structure for table searches +-- + +CREATE TABLE searches ( + search_id integer NOT NULL PRIMARY KEY, + user_id integer NOT NULL DEFAULT '0', + "type" smallint NOT NULL DEFAULT '0', + name varchar(128) NOT NULL, + data text NOT NULL +); + +CREATE UNIQUE INDEX ix_searches_user_type_name (user_id, type, name); diff --git a/SQL/sqlite.update.sql b/SQL/sqlite.update.sql index 8d5163f47..41ab0200d 100644 --- a/SQL/sqlite.update.sql +++ b/SQL/sqlite.update.sql @@ -227,6 +227,7 @@ DELETE FROM messages; DELETE FROM cache; CREATE INDEX ix_contactgroupmembers_contact_id ON contactgroupmembers (contact_id); + -- Updates from version 0.6-stable CREATE TABLE dictionary ( @@ -236,3 +237,13 @@ CREATE TABLE dictionary ( ); CREATE UNIQUE INDEX ix_dictionary_user_language ON dictionary (user_id, "language"); + +CREATE TABLE searches ( + search_id integer NOT NULL PRIMARY KEY, + user_id integer NOT NULL DEFAULT '0', + "type" smallint NOT NULL DEFAULT '0', + name varchar(128) NOT NULL, + data text NOT NULL +); + +CREATE UNIQUE INDEX ix_searches_user_type_name (user_id, type, name); diff --git a/config/db.inc.php.dist b/config/db.inc.php.dist index 78cd96882..12304cd96 100644 --- a/config/db.inc.php.dist +++ b/config/db.inc.php.dist @@ -68,6 +68,8 @@ $rcmail_config['db_sequence_cache'] = 'cache_ids'; $rcmail_config['db_sequence_messages'] = 'message_ids'; +$rcmail_config['db_sequence_searches'] = 'search_ids'; + // end db config file diff --git a/program/include/rcube_user.php b/program/include/rcube_user.php index dc5767d14..90edad6e9 100644 --- a/program/include/rcube_user.php +++ b/program/include/rcube_user.php @@ -47,6 +47,8 @@ class rcube_user */ private $rc; + const SEARCH_ADDRESSBOOK = 1; + const SEARCH_MAIL = 2; /** * Object constructor @@ -551,4 +553,129 @@ class rcube_user return empty($plugin['email']) ? NULL : $plugin['email']; } + + /** + * Return a list of saved searches linked with this user + * + * @param int $type Search type + * + * @return array List of saved searches indexed by search ID + */ + function list_searches($type) + { + $plugin = $this->rc->plugins->exec_hook('saved_search_list', array('type' => $type)); + + if ($plugin['abort']) { + return (array) $plugin['result']; + } + + $result = array(); + + $sql_result = $this->db->query( + "SELECT search_id AS id, ".$this->db->quoteIdentifier('name') + ." FROM ".get_table_name('searches') + ." WHERE user_id = ?" + ." AND ".$this->db->quoteIdentifier('type')." = ?" + ." ORDER BY ".$this->db->quoteIdentifier('name'), + (int) $this->ID, (int) $type); + + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + $sql_arr['data'] = unserialize($sql_arr['data']); + $result[$sql_arr['id']] = $sql_arr; + } + + return $result; + } + + + /** + * Return saved search data. + * + * @param int $id Row identifier + * + * @return array Data + */ + function get_search($id) + { + $plugin = $this->rc->plugins->exec_hook('saved_search_get', array('id' => $id)); + + if ($plugin['abort']) { + return $plugin['result']; + } + + $sql_result = $this->db->query( + "SELECT ".$this->db->quoteIdentifier('name') + .", ".$this->db->quoteIdentifier('data') + .", ".$this->db->quoteIdentifier('type') + ." FROM ".get_table_name('searches') + ." WHERE user_id = ?" + ." AND search_id = ?", + (int) $this->ID, (int) $id); + + while ($sql_arr = $this->db->fetch_assoc($sql_result)) { + return array( + 'id' => $id, + 'name' => $sql_arr['name'], + 'type' => $sql_arr['type'], + 'data' => unserialize($sql_arr['data']), + ); + } + + return null; + } + + + /** + * Deletes given saved search record + * + * @param int $sid Search ID + * + * @return boolean True if deleted successfully, false if nothing changed + */ + function delete_search($sid) + { + if (!$this->ID) + return false; + + $this->db->query( + "DELETE FROM ".get_table_name('searches') + ." WHERE user_id = ?" + ." AND search_id = ?", + (int) $this->ID, $sid); + + return $this->db->affected_rows(); + } + + + /** + * Create a new saved search record linked with this user + * + * @param array $data Hash array with col->value pairs to save + * + * @return int The inserted search ID or false on error + */ + function insert_search($data) + { + if (!$this->ID) + return false; + + $insert_cols[] = 'user_id'; + $insert_values[] = (int) $this->ID; + $insert_cols[] = $this->db->quoteIdentifier('type'); + $insert_values[] = (int) $data['type']; + $insert_cols[] = $this->db->quoteIdentifier('name'); + $insert_values[] = $data['name']; + $insert_cols[] = $this->db->quoteIdentifier('data'); + $insert_values[] = serialize($data['data']); + + $sql = "INSERT INTO ".get_table_name('searches') + ." (".join(', ', $insert_cols).")" + ." VALUES (".join(', ', array_pad(array(), sizeof($insert_values), '?')).")"; + + call_user_func_array(array($this->db, 'query'), + array_merge(array($sql), $insert_values)); + + return $this->db->insert_id('searches'); + } + } diff --git a/program/js/app.js b/program/js/app.js index e671ce434..002f345ee 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -328,7 +328,7 @@ function rcube_webmail() this.enable_command('export', true); this.enable_command('add', 'import', this.env.writable_source); - this.enable_command('list', 'listgroup', 'advanced-search', true); + this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true); // load contacts of selected source if (!this.env.action) @@ -524,21 +524,15 @@ function rcube_webmail() break; case 'list': - if (this.task=='mail') { - if (!this.env.search_request || (props && props != this.env.mailbox)) - this.reset_qsearch(); - + this.reset_qsearch(); + if (this.task == 'mail') { this.list_mailbox(props); if (this.env.trash_mailbox && !this.env.flag_for_deletion) this.set_alttext('delete', this.env.mailbox != this.env.trash_mailbox ? 'movemessagetotrash' : 'deletemessage'); } else if (this.task == 'addressbook') { - if (!this.env.search_request || (props != this.env.source)) - this.reset_qsearch(); - this.list_contacts(props); - this.enable_command('add', 'import', this.env.writable_source); } break; @@ -1008,6 +1002,7 @@ function rcube_webmail() break; case 'listgroup': + this.reset_qsearch(); this.list_contacts(props.source, props.id); break; @@ -1994,7 +1989,7 @@ function rcube_webmail() if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort)) url += '&_refresh=1'; - this.select_folder(mbox, this.env.mailbox); + this.select_folder(mbox); this.env.mailbox = mbox; // load message list remotely @@ -3455,6 +3450,7 @@ function rcube_webmail() this.env.qsearch = null; this.env.search_request = null; + this.env.search_id = null; }; this.sent_successfully = function(type, msg) @@ -3829,7 +3825,7 @@ function rcube_webmail() this.list_contacts = function(src, group, page) { - var add_url = '', + var folder, add_url = '', target = window; if (!src) @@ -3845,7 +3841,12 @@ function rcube_webmail() else if (group != this.env.group) page = this.env.current_page = 1; - this.select_folder((group ? 'G'+src+group : src), (this.env.group ? 'G'+this.env.source+this.env.group : this.env.source)); + if (this.env.search_id) + folder = 'S'+this.env.search_id; + else + folder = group ? 'G'+src+group : src; + + this.select_folder(folder); this.env.source = src; this.env.group = group; @@ -3890,8 +3891,8 @@ function rcube_webmail() if (group) url += '&_gid='+group; - // also send search request to get the right messages - if (this.env.search_request) + // also send search request to get the right messages + if (this.env.search_request) url += '&_search='+this.env.search_request; this.http_request('list', url, lock); @@ -4092,19 +4093,7 @@ function rcube_webmail() this.group_create = function() { - if (!this.gui_objects.folderlist) - return; - - if (!this.name_input) { - this.name_input = $('').attr('type', 'text'); - this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); - this.name_input_li = $('
  • ').addClass('contactgroup').append(this.name_input); - - var li = this.get_folder_li(this.env.source) - this.name_input_li.insertAfter(li); - } - - this.name_input.select().focus(); + this.add_input_row('contactgroup'); }; this.group_rename = function() @@ -4150,18 +4139,40 @@ function rcube_webmail() this.list_contacts(prop.source, 0); }; + // @TODO: maybe it would be better to use popup instead of inserting input to the list? + this.add_input_row = function(type) + { + if (!this.gui_objects.folderlist) + return; + + if (!this.name_input) { + this.name_input = $('').attr('type', 'text').data('tt', type); + this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); + this.name_input_li = $('
  • ').addClass(type).append(this.name_input); + + var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source); + this.name_input_li.insertAfter(li); + } + + this.name_input.select().focus(); + }; + // handler for keyboard events on the input field this.add_input_keydown = function(e) { - var key = rcube_event.get_keycode(e); + var key = rcube_event.get_keycode(e), + input = $(e.target), itype = input.data('tt'); // enter if (key == 13) { - var newname = this.name_input.val(); + var newname = input.val(); if (newname) { var lock = this.set_busy(true, 'loading'); - if (this.env.group_renaming) + + if (itype == 'contactsearch') + this.http_post('search-create', '_search='+urlencode(this.env.search_request)+'&_name='+urlencode(newname), lock); + else if (this.env.group_renaming) this.http_post('group-rename', '_source='+urlencode(this.env.source)+'&_gid='+urlencode(this.env.group)+'&_name='+urlencode(newname), lock); else this.http_post('group-create', '_source='+urlencode(this.env.source)+'&_name='+urlencode(newname), lock); @@ -4477,11 +4488,106 @@ function rcube_webmail() // unselect directory/group this.unselect_directory = function() { - if (this.env.address_sources.length > 1 || this.env.group != '') { - this.select_folder('', (this.env.group ? 'G'+this.env.source+this.env.group : this.env.source)); - this.env.group = ''; - this.env.source = ''; + this.select_folder(''); + this.enable_command('search-delete', false); + }; + + // callback for creating a new saved search record + this.insert_saved_search = function(name, id) + { + this.reset_add_input(); + + var key = 'S'+id, + link = $('').attr('href', '#') + .attr('rel', id) + .click(function() { return rcmail.command('listsearch', id, this); }) + .html(name), + li = $('
  • ').attr({id: 'rcmli'+key.replace(this.identifier_expr, '_'), 'class': 'contactsearch'}) + .append(link), + prop = {name:name, id:id, li:li[0]}; + + this.add_saved_search_row(prop, li); + this.select_folder('S'+id); + this.enable_command('search-delete', true); + this.env.search_id = id; + + this.triggerEvent('abook_search_insert', prop); + }; + + // add saved search row to the list, with sorting + this.add_saved_search_row = function(prop, li, reloc) + { + var row, sibling, name = prop.name.toUpperCase(); + + // When renaming groups, we need to remove it from DOM and insert it in the proper place + if (reloc) { + row = li.clone(true); + li.remove(); + } + else + row = li; + + $('li[class~="contactsearch"]', this.gui_objects.folderlist).each(function(i, elem) { + if (!sibling) + sibling = this.previousSibling; + + if (name >= $(this).text().toUpperCase()) + sibling = elem; + else + return false; + }); + + if (sibling) + row.insertAfter(sibling); + else + row.appendTo(this.gui_objects.folderlist); + }; + + // creates an input for saved search name + this.search_create = function() + { + this.add_input_row('contactsearch'); + }; + + this.search_delete = function() + { + if (this.env.search_request) { + var lock = this.set_busy(true, 'savedsearchdeleting'); + this.http_post('search-delete', '_sid='+urlencode(this.env.search_id), lock); + } + }; + + // callback from server upon search-delete command + this.remove_search_item = function(id) + { + var li, key = 'S'+id; + if ((li = this.get_folder_li(key))) { + this.triggerEvent('search_delete', { id:id, li:li }); + + li.parentNode.removeChild(li); + } + + this.env.search_id = null; + this.env.search_request = null; + this.list_contacts_clear(); + this.reset_qsearch(); + this.enable_command('search-delete', 'search-create', false); + }; + + this.listsearch = function(id) + { + var folder, lock = this.set_busy(true, 'searching'); + + if (this.contact_list) { + this.list_contacts_clear(); } + + this.reset_qsearch(); + this.select_folder('S'+id); + + // reset vars + this.env.current_page = 1; + this.http_request('search', '_sid='+urlencode(id), lock); }; @@ -5231,20 +5337,20 @@ function rcube_webmail() }; // mark a mailbox as selected and set environment variable - this.select_folder = function(name, old, prefix) + this.select_folder = function(name, prefix) { if (this.gui_objects.folderlist) { var current_li, target_li; - if ((current_li = this.get_folder_li(old, prefix))) { - $(current_li).removeClass('selected').addClass('unfocused'); + if ((current_li = $('li.selected', this.gui_objects.folderlist))) { + current_li.removeClass('selected').addClass('unfocused'); } if ((target_li = this.get_folder_li(name, prefix))) { $(target_li).removeClass('unfocused').addClass('selected'); } // trigger event hook - this.triggerEvent('selectfolder', { folder:name, old:old, prefix:prefix }); + this.triggerEvent('selectfolder', { folder:name, prefix:prefix }); } }; @@ -5791,6 +5897,8 @@ function rcube_webmail() this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0)); if (response.action == 'list' || response.action == 'search') { + this.enable_command('search-create', this.env.source == ''); + this.enable_command('search-delete', this.env.search_id); this.update_group_commands(); this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount }); } diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 7facc12df..17544bb6d 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -140,7 +140,7 @@ $labels['markread'] = 'As read'; $labels['markunread'] = 'As unread'; $labels['markflagged'] = 'As flagged'; $labels['markunflagged'] = 'As unflagged'; -$labels['messageactions'] = 'More actions...'; +$labels['moreactions'] = 'More actions...'; $labels['select'] = 'Select'; $labels['all'] = 'All'; @@ -317,7 +317,6 @@ $labels['print'] = 'Print'; $labels['export'] = 'Export'; $labels['exportvcards'] = 'Export contacts in vCard format'; $labels['newcontactgroup'] = 'Create new contact group'; -$labels['groupactions'] = 'Actions for contact groups...'; $labels['grouprename'] = 'Rename group'; $labels['groupdelete'] = 'Delete group'; @@ -330,6 +329,9 @@ $labels['group'] = 'Group'; $labels['groups'] = 'Groups'; $labels['personaladrbook'] = 'Personal Addresses'; +$labels['searchsave'] = 'Save search'; +$labels['searchdelete'] = 'Delete search'; + $labels['import'] = 'Import'; $labels['importcontacts'] = 'Import contacts'; $labels['importfromfile'] = 'Import from file:'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index c871ca53e..65bc9ca6a 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -76,10 +76,10 @@ $messages['nosubjectwarning'] = 'The "Subject" field is empty. Would you like t $messages['nobodywarning'] = 'Send this message without text?'; $messages['notsentwarning'] = 'Message has not been sent. Do you want to discard your message?'; $messages['noldapserver'] = 'Please select an ldap server to search.'; -$messages['nocontactsreturned'] = 'No contacts were found.'; $messages['nosearchname'] = 'Please enter a contact name or email address.'; $messages['notuploadedwarning'] = 'Not all attachments have been uploaded yet. Please wait or cancel the upload.'; $messages['searchsuccessful'] = '$nr messages found.'; +$messages['contactsearchsuccessful'] = '$nr contacts found.'; $messages['searchnomatch'] = 'Search returned no matches.'; $messages['searching'] = 'Searching...'; $messages['checking'] = 'Checking...'; @@ -139,6 +139,10 @@ $messages['contactrestored'] = 'Contact(s) restored successfully.'; $messages['groupdeleted'] = 'Group deleted successfully.'; $messages['grouprenamed'] = 'Group renamed successfully.'; $messages['groupcreated'] = 'Group created successfully.'; +$messages['savedsearchdeleted'] = 'Saved search deleted successfully.'; +$messages['savedsearchdeleteerror'] = 'Could not delete saved search.'; +$messages['savedsearchcreated'] = 'Saved search created successfully.'; +$messages['savedsearchcreateerror'] = 'Could not create saved search.'; $messages['messagedeleted'] = 'Message(s) deleted successfully.'; $messages['messagemoved'] = 'Message(s) moved successfully.'; $messages['messagecopied'] = 'Message(s) copied successfully.'; diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 55d4255aa..b290bbb2d 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -227,7 +227,32 @@ function rcmail_directory_list($attrib) $out = $groupdata['out']; } - $OUTPUT->set_env('contactgroups', $jsdata); + $line_templ = html::tag('li', array( + 'id' => 'rcmliS%s', 'class' => '%s'), + html::a(array('href' => '#', 'rel' => 'S%s', + 'onclick' => "return ".JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s')); + + // Saved searches + $sources = $RCMAIL->user->list_searches(rcube_user::SEARCH_ADDRESSBOOK); + foreach ($sources as $j => $source) { + $id = $source['id']; + $js_id = JQ($id); + + // set class name(s) + $class_name = 'contactsearch'; + if ($current === $id) + $class_name .= ' selected'; + if ($source['class_name']) + $class_name .= ' ' . $source['class_name']; + + $out .= sprintf($line_templ, + html_identifier($id), + $class_name, + $id, + $js_id, (!empty($source['name']) ? Q($source['name']) : Q($id))); + } + + $OUTPUT->set_env('contactgroups', $jsdata); $OUTPUT->add_gui_object('folderlist', $attrib['id']); // add some labels to client $OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember'); @@ -745,4 +770,6 @@ $RCMAIL->register_action_map(array( 'group-delete' => 'groups.inc', 'group-addmembers' => 'groups.inc', 'group-delmembers' => 'groups.inc', + 'search-create' => 'search.inc', + 'search-delete' => 'search.inc', )); diff --git a/program/steps/addressbook/search.inc b/program/steps/addressbook/search.inc index 352556de0..ad1df9792 100644 --- a/program/steps/addressbook/search.inc +++ b/program/steps/addressbook/search.inc @@ -21,6 +21,60 @@ */ +if ($RCMAIL->action == 'search-create') { + $id = get_input_value('_search', RCUBE_INPUT_POST); + $name = get_input_value('_name', RCUBE_INPUT_POST, true); + + if (($params = $_SESSION['search_params']) && $params['id'] == $id) { + + $data = array( + 'type' => rcube_user::SEARCH_ADDRESSBOOK, + 'name' => $name, + 'data' => array( + 'fields' => $params['data'][0], + 'search' => $params['data'][1], + ), + ); + + $plugin = $RCMAIL->plugins->exec_hook('saved_search_create', array('data' => $data)); + + if (!$plugin['abort']) + $result = $RCMAIL->user->insert_search($plugin['data']); + else + $result = $plugin['result']; + } + + if ($result) { + $OUTPUT->show_message('savedsearchcreated', 'confirmation'); + $OUTPUT->command('insert_saved_search', Q($name), Q($result)); + } + else + $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'savedsearchcreateerror', 'error'); + + $OUTPUT->send(); +} + +if ($RCMAIL->action == 'search-delete') { + $id = get_input_value('_sid', RCUBE_INPUT_POST); + + $plugin = $RCMAIL->plugins->exec_hook('saved_search_delete', array('id' => $id)); + + if (!$plugin['abort']) + $result = $RCMAIL->user->delete_search($id); + else + $result = $plugin['result']; + + if ($result) { + $OUTPUT->show_message('savedsearchdeleted', 'confirmation'); + $OUTPUT->command('remove_search_item', Q($id)); + } + else + $OUTPUT->show_message($plugin['message'] ? $plugin['message'] : 'savedsearchdeleteerror', 'error'); + + $OUTPUT->send(); +} + + if (!isset($_GET['_form'])) { rcmail_contact_search(); } @@ -34,9 +88,15 @@ function rcmail_contact_search() global $RCMAIL, $OUTPUT, $CONFIG, $SEARCH_MODS_DEFAULT; $adv = isset($_POST['_adv']); + $sid = get_input_value('_sid', RCUBE_INPUT_GET); + // get search criteria from saved search + if ($sid && ($search = $RCMAIL->user->get_search($sid))) { + $fields = $search['data']['fields']; + $search = $search['data']['search']; + } // get fields/values from advanced search form - if ($adv) { + else if ($adv) { foreach (array_keys($_POST) as $key) { $s = trim(get_input_value($key, RCUBE_INPUT_POST, true)); if (strlen($s) && preg_match('/^_search_([a-zA-Z0-9_-]+)$/', $key, $m)) { @@ -145,6 +205,7 @@ function rcmail_contact_search() // save search settings in session $_SESSION['search'][$search_request] = $search_set; + $_SESSION['search_params'] = array('id' => $search_request, 'data' => array($fields, $search)); $_SESSION['page'] = 1; if ($adv) @@ -153,6 +214,7 @@ function rcmail_contact_search() if ($result->count > 0) { // create javascript list rcmail_js_contacts_list($result); + $OUTPUT->show_message('contactsearchsuccessful', 'confirmation', array('nr' => $result->count)); } else { $OUTPUT->show_message('nocontactsfound', 'notice'); @@ -162,9 +224,14 @@ function rcmail_contact_search() $OUTPUT->command('set_env', 'search_request', $search_request); $OUTPUT->command('set_env', 'pagecount', ceil($result->count / $CONFIG['pagesize'])); $OUTPUT->command('set_rowcount', rcmail_get_rowcount_text($result)); + // Re-set current source + $OUTPUT->command('set_env', 'search_id', $sid); + $OUTPUT->command('set_env', 'source', ''); + $OUTPUT->command('set_env', 'group', ''); // unselect currently selected directory/group - $OUTPUT->command('unselect_directory'); + if (!$sid) + $OUTPUT->command('unselect_directory'); $OUTPUT->command('update_group_commands'); // send response diff --git a/skins/default/addressbook.css b/skins/default/addressbook.css index f3b52c806..c604c7549 100644 --- a/skins/default/addressbook.css +++ b/skins/default/addressbook.css @@ -109,7 +109,8 @@ #directorylistbox input { - margin: 2px; + margin: 0px; + font-size: 11px; width: 90%; } @@ -165,7 +166,12 @@ #directorylist li.contactgroup { padding-left: 15px; - background-position: 20px -144px; + background-position: 20px -143px; +} + +#directorylist li.contactsearch +{ + background-position: 6px -162px; } #directorylist li.selected diff --git a/skins/default/images/icons/folders.gif b/skins/default/images/icons/folders.gif index 0fccb2c18a1d19c7d537552a7c3713123c3726d3..1002cb770ccf84762039fa605630337d06529e33 100644 GIT binary patch literal 2441 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 diff --git a/skins/default/images/icons/folders.png b/skins/default/images/icons/folders.png index 5013318f8584a6c7d9661d2d3d7ffa245e0b4475..d8b9e32e16ef12e47c921e582f7ef7961f122e9b 100644 GIT binary patch literal 5332 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 - + diff --git a/skins/default/templates/addressbook.html b/skins/default/templates/addressbook.html index a85c889c0..1930debf1 100644 --- a/skins/default/templates/addressbook.html +++ b/skins/default/templates/addressbook.html @@ -58,7 +58,7 @@
    - +
    @@ -99,6 +99,8 @@
    • +
    • +