diff --git a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php index a9d2582886..7228160ea3 100644 --- a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php +++ b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php @@ -70,6 +70,8 @@ * 'selected' => // O array with selected id's * 'checkboxes' => // O array with checkbox id as key and boolean checked value * 'select_all' => // O boolean value of select_all checkbox, reference to above value for key 'select_all' + * 'favorites' => // I boolean|array True to enable favorites, or an array of additional, app specific settings to include + * in the saved filters (eg: pm_id) */ class etemplate_widget_nextmatch extends etemplate_widget { @@ -135,6 +137,13 @@ class etemplate_widget_nextmatch extends etemplate_widget } } } + + // Favorite group for admins + if($GLOBALS['egw_info']['apps']['admin'] && $value['favorites']) + { + self::$request->sel_options[$form_name]['favorite']['group'] = array('all' => lang('All users')) + + etemplate_widget_menupopup::typeOptions('select-account',',groups'); + } foreach($value as $name => $_value) { if(strpos($name, 'options-') !== false) @@ -735,6 +744,7 @@ class etemplate_widget_nextmatch extends etemplate_widget { $form_name = self::form_name($cname, $this->id, $expand); $value = self::get_array($content, $form_name); + list($app) = explode('.',$this->attrs['template']); // On client, rows does not get its own namespace, but all apps are expecting it $value['rows'] = $value; @@ -750,7 +760,6 @@ class etemplate_widget_nextmatch extends etemplate_widget if($value['as_default']) { unset($value['as_default']); - list($app) = explode('.',$this->attrs['template']); if($GLOBALS['egw_info']['user']['apps']['admin'] && $app) { $pref_name = 'nextmatch-' . (isset($value['columnselection_pref']) ? $value['columnselection_pref'] : $this->attrs['template']); @@ -770,6 +779,46 @@ class etemplate_widget_nextmatch extends etemplate_widget $validated[$form_name] = $value; } + + /** + * Create or delete a favorite for multiple users + * + * Need to be an admin or it will just do nothing quietly + * + * @param $app Current application, needed to save preference + * @param $name String Name of the favorite + * @param $action String add or delete + * @param $group int|String ID of the group to create the favorite for, or All for all users + * @param $filters Array of key => value pairs for the filter + * + * @return boolean Success + */ + public static function ajax_set_favorite($app, $name, $action, $group, $filters = array()) + { + $pref_name = "favorite_".$name; + if($group && $GLOBALS['egw']['apps']['admin']) + { + $prefs = new preferences(is_numeric($group) ? $group: $GLOBALS['egw_info']['user']['account_id']); + } + else + { + $prefs =& $GLOBALS['egw']->preferences; + $type = 'user'; + } + $type = $group == "all" ? "default" : "user"; + if($action == "add") + { + $prefs->add($app,$pref_name,$filters,$type); + } + else if ($action == "delete") + { + $prefs->delete($app,$pref_name, $type); + } + $prefs->save_repository(false,$type); + + egw_json_response::get()->data(true); + } + /** * Run a given method on all children * diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js index a8c865ff8e..28f48518b4 100644 --- a/etemplate/js/et2_extension_nextmatch.js +++ b/etemplate/js/et2_extension_nextmatch.js @@ -247,12 +247,36 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput], { applyFilters: function() { this.egw().debug("info", "Changing nextmatch filters to ", this.activeFilters); + + if(typeof this.activeFilters == "undefined") + { + this.activeFilters = {col_filter: {}}; + } + if(typeof this.activeFilters.col_filter == "undefined") + { + this.activeFilters.col_filter = {}; + } // Update the filters in the grid controller this.controller.setFilters(this.activeFilters); + // Update the header + this.header.setFilters(this.activeFilters); + + // Update any column filters + this.iterateOver(function(column) { + if(typeof column.set_value != "undefined" && column.id) + { + column.set_value(typeof this[column.id] == "undefined" ? "" : this[column.id]); + } + if (column.id && typeof column.get_value == "function") + { + this[column.id] = column.get_value(); + } + }, this.activeFilters.col_filter, et2_INextmatchHeader); + // Trigger an update - this.controller.update(); + this.controller.update(true); }, /** @@ -269,15 +293,16 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput], { refresh: function(_row_ids, _type) { if (typeof _type == 'undefined') _type = 'edit'; if (typeof _row_ids == 'string') _rowids = [_row_ids]; + if (typeof _row_ids == "undefined") + { + this.applyFilters(); + return; + } // Use jsapi data module to update var list = et2_csvSplit(this.options.settings.get_rows, 2, "."); var app = list[0]; - if(typeof _row_ids == "string") - { - _row_ids = _row_ids.split(","); - } id_loop: for(var i = 0; i < _row_ids.length; i++) { @@ -922,7 +947,6 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput], { var refresh_preference = "nextmatch-" + this.options.settings.columnselection_pref + "-autorefresh"; var app = this.options.template.split("."); return this.egw().preference(refresh_preference,app[0]); - console.log(this); }, /** @@ -1061,6 +1085,9 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, { destroy: function() { this.nextmatch = null; this.div = null; + this.favorites = null; + this.favorites_popup.dialog("destroy"); + this.favorites_popup = null; }, setNextmatch: function(nextmatch) { @@ -1124,6 +1151,9 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, { self.nextmatch.applyFilters(); } } + + // Set activeFilters to current value + self.nextmatch.activeFilters[_widget.id] = _widget.getValue(); }, this, et2_inputWidget); } } @@ -1173,9 +1203,12 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, { // Search - this.search = et2_createWidget("textbox", {"blur":egw.lang("search")}, this); + this.search = et2_createWidget("textbox", {"id":"search","blur":egw.lang("search")}, this); this.search.input.attr("type", "search"); this.search.input.val(settings.search); + + // Set activeFilters to current value + this.nextmatch.activeFilters.search = settings.search; this.search_button = et2_createWidget("button", {"label":">"}, this); this.search_button.onclick = function(event) { @@ -1220,6 +1253,8 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, { event.data.activeFilters.searchletter = (letter == "" ? false : letter); event.data.applyFilters(); }); + // Set activeFilters to current value + this.nextmatch.activeFilters.searchletter = current_letter; } }, @@ -1283,6 +1318,9 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, { // Set value select.set_value(value); + // Set activeFilters to current value + this.nextmatch.activeFilters[select.id] = select.get_value(); + // Set onChange var input = select.input; @@ -1324,12 +1362,20 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, { /** * Set up the favorites UI control * - * Favorites are implemented by saving the values for [column] filters. Filters used/stored are - * listed in the favorites nm setting. + * Favorites are implemented by saving the values for [column] filters. Filters are stored + * in preferences, with the name favorite_. The favorite favorite used for clicking on + * the filter button is stored in nextmatch--favorite. + * + * @param filters Array|boolean The nextmatch setting for favorites. Either true, or a list of + * additional fields/settings to add in to the favorite. */ _setup_favorites: function(filters) { - var self = this; + // Some convenient variables, used in closures / event handlers + var header = this; + var nextmatch = this.nextmatch; var nm_div = this.nextmatch.div; + var favorite_prefix = "favorite_"; + var favorite_preference = "nextmatch-" + nextmatch.options.settings.columnselection_pref + "-favorite"; if(typeof filters == "undefined") { @@ -1337,23 +1383,254 @@ var et2_nextmatch_header_bar = et2_DOMWidget.extend(et2_INextmatchHeader, { return; } + var list = et2_csvSplit(this.options.get_rows, 2, "."); var app = list[0]; - var stored_filters = this.egw().preference("nextmatch-"+this.options.columnselection_pref+"-favorites", app); + + // Load saved favorites + var stored_filters = {}; + var preferences = this.egw().preference("*",app); + for(var pref_name in preferences) + { + if(pref_name.indexOf(favorite_prefix) == 0) + { + stored_filters[pref_name.substr(favorite_prefix.length)] = preferences[pref_name]; + } + } if(typeof stored_filters == "undefined" || !stored_filters) { stored_filters = {}; } - /* + + // Create filter options + var init_filters = function() + { + var options = {}; + for(var name in stored_filters) + { + options[name] = ""+name; + } + options.add = "Add current"; + return options; + }; + + // Favorite dropdown button this.favorites = et2_createWidget("dropdown_button", { - id: "nm_favorites", + id: "favorite[button]", + label: "", label_updates: false, - image: "etemplate/filter", - onclick: self.applyFilters, - select_options: stored_filters + image: "etemplate/fav_filter", + tooltip: "Favorite queries" }, this); + + this.favorites.onclick= function(node) { + // Apply preferred filter - make sure it's an object, and not a reference + nextmatch.activeFilters = jQuery.extend({},stored_filters[header.egw().preference(favorite_preference,app)]); + nextmatch.applyFilters(); + }; + + var preferred = this.egw().preference(favorite_preference, app); + this.favorites.set_value(preferred && stored_filters[preferred] ? preferred : ""); + this.favorites.set_select_options(init_filters()); + + // Apply the favorite when you pick from the list + this.favorites.change = function(selected_node) { + if(this.get_value() == "add") + { + // Get current filters + header.favorites_popup.current_filters = $j.extend({},nextmatch.activeFilters); + + // Add in application's settings + if(filters != true) + { + for(var i = 0; i < filters.length; i++) + { + header.favorites_popup.current_filters[filters[i]] = nextmatch.options.settings[filters[i]]; + } + } + // Remove some internal values + delete header.favorites_popup.current_filters[header.favorites.id]; + delete header.favorites_popup.current_filters[header.favorites_popup.group.id]; + + // Make sure it's an object + header.favorites_popup.current_filters = jQuery.extend({},header.favorites_popup.current_filters); + + // Update popup with current set filters (more for debug than user) + var filter_list = []; + var add_to_popup = function(arr) { + filter_list.push(""); + } + add_to_popup(header.favorites_popup.current_filters); + $j("#nm_favorites_popup_filters",header.favorites_popup) + .replaceWith( + $j(filter_list.join("")).attr("id","nm_favorites_popup_filters") + ); + + // Popup + header.favorites_popup.dialog("open"); + } + else if(stored_filters[this.get_value()]) + { + // Apply selected filter - make sure it's an object, and not a reference + nextmatch.activeFilters = jQuery.extend({},stored_filters[this.get_value()]); + nextmatch.applyFilters(); + } + }; + + // Set radio to current value + $j("input[value='"+ preferred +"']:radio", this.favorites.menu).attr("checked",true); + + // Add a listener on the radio buttons to set default filter + $j(this.favorites.menu).on("click","input:radio",function(event){ + // Don't do the menu + event.stopImmediatePropagation(); + + // Save as default favorite - used when you click the button + header.egw().set_preference(app,favorite_preference,$j(this).val()); + + // Close the menu + header.favorites.menu.hide(); + + // Some user feedback + header.favorites.button.addClass("ui-state-active", 500,"swing",function(){ + header.favorites.button.removeClass("ui-state-active",2000); + }); + + }); + + // Add into header $j(this.favorites.getDOMNode(this.favorites)).insertAfter(this.count).css("float","right"); - */ + + + // Create popup + this.favorites_popup = $j('
\ +
\ + ' + + + '\ +
\ +
    \ +\ +
'); + this.favorites_popup.name = et2_createWidget("text",{id:"favorite[name]"},this); + $j("name",this.favorites_popup).replaceWith(this.favorites_popup.name.getDOMNode()); + + // Add some controls if user is an admin + var apps = this.egw().user('apps'); + if(apps['admin']) + { + this.favorites_popup.group = et2_createWidget("select-account",{ + id: "favorite[group]", + account_type: "groups", + empty_label: "Groups", + no_lang: true + },this); + $j("#nm_favorites_popup_admin",this.favorites_popup) + .append(this.favorites_popup.group.getDOMNode(this.favorites_popup.group)); + + } + var buttons = {}; + buttons[this.egw().lang("save")] = function() { + // Add a new favorite + var name = $j("#name",this); + + if(name.val()) + { + + // Add to the list + stored_filters[name.val()] = header.favorites_popup.current_filters; + header.favorites.set_select_options(init_filters()); + + var favorite_pref = favorite_prefix+name.val(); + + // Save to preferences + if(typeof header.favorites_popup.group != "undefined") + { + // Admin stuff - save preference server side + var request = new egw_json_request("etemplate_widget_nextmatch::ajax_set_favorite::etemplate", + [app, name.val(), "add", header.favorites_popup.group.get_value(), header.favorites_popup.current_filters], + header + ); + request.sendRequest(true, function(result) { + if(result) + { + // Do something nice and confirmy if result is true - not really needed + } + }, header); + } + else + { + // Normal user - just save to preferences client side + header.egw().set_preference(app,favorite_pref,header.favorites_popup.current_filters); + } + delete header.favorites_popup.current_filters; + } + // Reset form + name.val(""); + header.favorites_popup.group.set_value(""); + $j("#filters",header.favorites_popup).empty(); + + $j(this).dialog("close"); + }; + buttons[this.egw().lang("cancel")] = function() { + $j(this).dialog("close"); + }; + + this.favorites_popup.dialog({ + autoOpen: false, + modal: true, + buttons: buttons, + close: function() { + } + }); + }, + + /** + * Updates all the filter elements in the header + * + * Does not actually refresh the data, just sets values to match those given. + * Called by et2_nextmatch.applyFilters(). + * + * @param filters Array Key => Value pairs of current filters + */ + setFilters: function(filters) { + this.iterateOver(function(child) { + if(typeof child.set_value != "undefined" && child.id) + { + child.set_value(typeof this[child.id] == "undefined" ? "" : this[child.id]); + } + if(typeof child.get_value == "function" && child.id) + { + this[child.id] = child.get_value(); + } + }, filters); + + // Letter search + if(this.nextmatch.options.settings.lettersearch) + { + jQuery("td",this.lettersearch).removeClass("lettersearch_active"); + $j(filters.searchletter ? "td#"+filters.searchletter : "td.lettersearch[id='']").addClass("lettersearch_active"); + + // Set activeFilters to current value + filters.searchletter = $j("td.lettersearch_active").attr("id") + } + + // Remove some internal values + if(typeof this.favorites != "undefined") + { + delete filters[this.favorites.id]; + delete filters[this.favorites_popup.group.id]; + } }, /** diff --git a/etemplate/js/et2_widget_dropdown_button.js b/etemplate/js/et2_widget_dropdown_button.js index a1d1f4f9f1..d33be5cc38 100644 --- a/etemplate/js/et2_widget_dropdown_button.js +++ b/etemplate/js/et2_widget_dropdown_button.js @@ -287,6 +287,16 @@ var et2_dropdown_button = et2_inputWidget.extend({ this.change(selected_node); }, + attachToDOM: function() { + this._super.apply(this, arguments); + + // Move the parent's handler to the button, or we can't tell the difference between the clicks + $j(this.node).unbind("click.et2_baseWidget"); + this.button.bind("click.et2_baseWidget", this, function(e) { + return e.data.click.call(e.data, this); + }); + }, + set_label: function(_value) { if (this.button) { @@ -345,6 +355,10 @@ var et2_dropdown_button = et2_inputWidget.extend({ this.set_label(this.options.label); } } + }, + + getValue: function() { + return this.value; } }); diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index f8351af58a..fe78a96b63 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -189,6 +189,27 @@ button.et2_button_text:focus, input[type=button]:focus { outline: none; } +/** + * Drop down button + */ +.et2_dropdown button { + height: 3.0ex; + display: inline-block; + vertical-align: middle; + margin-right: -2px; + padding: 0px 1ex; +} +.et2_dropdown button > div { + vertical-align: middle; +} +.et2_dropdown button:last-child { + padding: 0px +} +.et2_dropdown + ul.ui-menu { + position: absolute; + z-index: 2; +} + /** * Color picker widget */ @@ -738,6 +759,19 @@ label input, label span, label div, label select, label textarea { .nextmatch_header > .filters .et2_button_icon { margin-top: 8px; } +/* Favorites */ +.nextmatch_header div#favorite\[button\]_wrapper { + margin-top: 5px; + vertical-align: middle; +} +#nm_favorites_popup_filters .filter_id, #nm_favorites_popup_filters .filter_value { + width: 10ex; + display: inline-block; +} +#favorite\[button\]_menu input, #favorite\[button\]_menu img { + margin-right: 2ex; +} + .nextmatch_sortheader { color: #003075; cursor: pointer; diff --git a/etemplate/templates/default/images/fav_filter.png b/etemplate/templates/default/images/fav_filter.png new file mode 100644 index 0000000000..397e078992 Binary files /dev/null and b/etemplate/templates/default/images/fav_filter.png differ